Россия, Звенигород |
Объекты XPCOM
16.6.4. Работа с запросами RDF при помощи интерфейсов XPCOM
Механизм шаблонов XUL – лишь один из способов формировать запросы к набору фактов RDF. Это декларативный подход, подобный языку SQL. Другой способ – самостоятельно перебирать факты RDF при помощи скрипта. Этот подход, аналогичный перемещению по структуре данных, можно назвать алгоритмическим. Он подразумевает использование множества интерфейсов XPCOM, предназначенных для работы с содержимым RDF. Эти интерфейсы облегчают перемещение между фактами, поэтому алгоритмический подход требует от разработчика меньше усилий, чем можно было бы ожидать.
В приложении NoteTaker используется один запрос, который было бы разумно реализовать на основе скрипта. Это запрос, выполняющий поиск заметки, существующей для данного URL. Шаблоны не подходят для решения этой задачи по следующим причинам:
- Результаты запроса должны быть использованы в скрипте JavaScript, а не в окне XUL.
- Запрос не предполагает визуального отображения результатов.
- Диалоговое окно позволяет при желании удалить параметры запроса HTTP GET из URL страницы. Для этого не6оходимо, чтобы по такому URL можно было найти заметку независимо от того, присутствует ли в ее URL строка параметров. Это требует операций со строками, которые не поддерживаются шаблонами, но легко могут быть выполнены в скрипте JavaScript.
Поиск выполняется при помощи метода resolve() объекта заметки в файле notes.js. Мы уже создали каркас этого метода в предыдущих лекциях, а теперь добавим его полную реализацию. Метод загружает данные RDF для нужной заметки в соответствующие свойства объекта заметки. Реализация метода показана в листинге 16.14.
resolve : function (url) { var ds = window.noteSession.datasource; var ns = "http://www.mozilla.org/notetaker-rdf#"; var rdf = Cc["@mozilla.org/rdf/rdf-service;1"]; rdf = rdf.getService(Ci.nsIRDFService); var container = Cc["@mozilla.org/rdf/container;1"]; container = container.getService(Ci.nsIRDFContainer); var cu = Cc["@mozilla.org/rdf/container-utils;1"]; cu = cu.getService(Ci.nsIRDFContainerUtils); var seq_node = rdf.GetResource("urn:notetaker:notes"); var url_node = rdf.GetResource(url); var chopped_node = rdf.GetResource(url.replace(/\?.*/,"")); var matching_node, prop_node, value_node; if (!cu.IsContainer(ds,seq_node)) { throw "Missing <Seq> 'urn:notetaker:notes' in " + noteSession.config_file; return; } container.Init(ds,seq_node); // Сначала попробовать полный URL, потом усеченный URL, если не найдены – заметки нет if ( container.IndexOf(url_node) != -1) { matching_node = url_node; this.url = url; this.chop_query = false; } else if ( container.IndexOf(chopped_node) != -1 ) { matching_node = chopped_node; this.url = url.replace(/\?.*/,""); } else { this.url = null; return; } else return; // Если заметка найдена, получить все ее свойства. var props = ["summary", "details", "width", "height", "top", "left"]; for (var i=0; i<props.length; i++) { pred_node = rdf.GetResource(ns + props[i]); value_node = ds.GetTarget(matching_node, pred_node, true); value_node = value_node.QueryInterface(Ci.nsIRDFLiteral); this[props[i]] = value_node.Value; } }Листинг 16.14. Выполнение запроса RDF из скрипта.
Прежде всего, метод создает три служебных объекта XPCOM для работы с RDF. Объект nsIRDFService используется для преобразования простой строки URL в объект nsIRDFResource, являющийся подтипом более общего типа nsIRDFNode. Большинство методов для работы с RDF принимают в качестве аргумента не простые строки, а объекты типа nsIRDFNode. Мы создаем такие объекты как для полных, так и для усеченных URL. Единственное применение объекта nsIContainerUtils в нашем методе – убедиться, что в файле notetaker.rdf содержится ресурс urn:notetaker:notes, который является контейнером. Если это условие не выполнено, работа метода прерывается и генерируется исключение. Затем мы используем интерфейс nsIRDFContainer для того, чтобы связать URI контейнера ( <Seq> ) с источником данных и инициализировать эту связь. Как правило, доступ к источнику данных осуществляется путем обращения к отдельным фактам. Интерфейс nsIRDFContainer позволяет работать с контейнером и его элементами как со структурой данных.
После инициализации служебных объектов мы переходим к выполнению собственно запроса, который в данном случае несложен. Его алгоритм таков: найти в источнике данных ресурс, соответствующий полному URL; если таковой отсутствует, найти ресурс, соответствующий URL без параметров; если и этот поиск завершился неудачей, отказаться от дальнейшего выполнения метода.
В оставшейся части метода мы извлекаем все факты, описывающие пары свойство/значение для данной заметки. Метод rdf.GetTarget() всегда возвращает объект nsIRDFNode, который необходимо преобразовать к тому типу, который мы используем для свойств заметки. Во всех случаях это простая строка (мы не храним размеры окна как целые). Наконец, полученные значения свойств присваиваются свойствам объекта заметки. Эта часть кода предполагает, что заметка описана в файле notetaker.rdf корректным образом.
Запрос, выполняемый в этом методе, основан на двух фактах и в целом аналогичен тем простым запросам, которые мы применяли в шаблонах, за исключением некоторых проверок в начале метода и использования двух различных URL для поиска.
Если попытаться протестировать метод resolve(), например, добавив код вида:
note.resolve("http://saturn/test1.html");
Система, скорее всего, выдаст неожиданные сообщения об ошибках. Как правило, ошибки такого рода возникают при первом обращении к интерфейсам для работы с RDF, однако могут возникать и позднее, и, что хуже всего, нерегулярно. Виновник ошибок находится за пределами объекта заметки – это объект noteSession. Источник данных инициализируется в функции init() в составе этого объекта следующим образом:
this.datasource = GetDataSource(url.spec);
Этот способ инициализации не подходит для наших задач. Он осуществляет асинхронную загрузку данных, поэтому хранилище данных заполняется постепенно. Тем временем скрипт переходит к выполнению дальнейших инструкций и, возможно, пытается запросить данные до того, как они загружены. Поэтому неудивительно, что методы RDF сообщают об отсутствии контейнеров и ресурсов, которые заведомо присутствуют в файле с данными. Решение проблемы – синхронная загрузка данных:
this.datasource = GetDataSourceBlocking(url.spec);
Это может привести к незначительной задержке при загрузке браузера, однако допустимо для нашего простого приложения.
Впрочем, мы могли бы избежать этой задержки, не отказываясь от асинхронной загрузки, но используя дополнительные интерфейсы nsIRequestObserver или nsIStreamListener компонента xml-datasource. С помощью наблюдателя или слушателя мы могли бы определить момент завершения загрузки. Некоторые из объектов XPCOM, созданных для этой цели, могли бы найти применение и в других методах. Сделав эти объекты доступными через объект noteSession, мы могли бы обеспечить возможность их повторного использования. Дальнейшие подробности реализации этой стратегии выходят за рамки данной книги.
В предыдущих лекциях мы написали код, который помещает данные объекта заметки в поля формы или HTML-документ, отображаемый в окне браузера. В этой лекции мы подключили объект заметки к хранилищу фактов RDF, основанному на постоянно хранимом файле. В результате данные ранее сохраненных заметок должны корректно отображаться на экране. Необходимо лишь внести небольшое исправление в функцию content_poll() находящуюся в файле toolbar_action.js. Необходимо заменить эту строку:
display_note()
на следующую:
if (note.url != null ) display_note()
Это позволит не отображать никаких дополнительных элементов для страниц, для которых не созданы заметки. Так гораздо лучше!
16.6.5. Когда перемещать данные, введенные пользователем, в хранилище RDF
NoteTaker позволяет не только просматривать уже сохраненные данные заметок, но и изменять или добавлять их. Последний раз мы работали с вводом данных в "Формы и меню" "Формы и меню", где введенные данные пересылались на Web-сервер. В этой лекции мы будем сохранять данные в хранилище файлов RDF и, в конечном счете, в локальном файле. Основная альтернатива этому решению – использование реляционной базы данных.
Для наших целей сохранить данные означает добавить их к источнику данных. После этого они будут находиться в оперативной памяти до тех пор, пока пользователь не выполнит какие-либо действия, приводящие к синхронизации хранилища фактов с файлом, или до завершения работы платформы. Какие именно действия пользователя должны приводить к сохранению данных на диске - решать разработчику.
Пользователь может вводить и изменять данные как в панели инструментов NoteTaker, так и в диалоговом окне. Мы последовательно рассмотрим эти варианты, начав с панели инструментов.
Пользователь может легко создать или изменить заметку, используя поля аннотации (summary) и ключевых слов (keyword) в панели инструментов. Любого изменения в поле аннотации или добавленного ключевого слова достаточно для того, чтобы заметка считалась измененной. Если пользователь изменяет эти поля, но не нажимает ни одной кнопки панели инструментов ( Edit, Save или Delete ), происходить ничего не должно. Поэтому данные, измененные пользователем, могут обрабатываться при помощи команд, доступных из панели инструментов. Прибегать к обработчикам событий onchange или другим подобным механизмам не требуется.
Панель Edit диалогового окна в этом отношении аналогична панели инструментов. Изменения, сделанные пользователем, должны сохраняться лишь в том случае, если пользователь нажимает кнопку OK. Если пользователь закрывает окно, нажав кнопку Cancel, изменения сохраняться не должны. Однако ситуация с панелью Keywords более сложна.
Панель Keywords позволяет пользователю добавить или удалить любое количество ключевых слов, используя соответствующие кнопки. Проблема состоит в следующем: где должны храниться эти изменения, пока диалоговое окно не закрыто? Если пользователь, в конечном счете, нажимает Cancel, добавленные слова не должны быть сохранены. Если пользователь принимает сделанные изменения, они должны быть сохранены.
Однако мы хотим, чтобы в процессе работы пользователя с окном вносимые изменения отображались в соответствующих элементах <listbox> и <tree>. Это означает, что ключевые слова должны добавляться к хранилищу RDF, даже если мы еще не знаем, примет пользователь эти изменения или отменит их.
Таким образом, мы столкнулись с проблемой отмены сделанных действий, иными словами – отката транзакций. Мы хотим немедленно добавлять ключевые слова к источнику данных, чтобы они были доступны всем элементам, использующим этот источник, однако нам необходимо иметь возможность удалить их, если пользователь, в конце концов, не принимает сделанных изменений. Для решения этой проблемы мы реализуем новый контроллер команды. Этот контроллер будет записывать выполняемые операции с ключевыми словами в буфер отмены. Если будет получена команда на отмену транзакции, контроллер, используя сохраненную информацию, сможет вернуть источник данных к исходному состоянию. Платформа Mozilla не предоставляет готового объекта с такой функциональностью, но в большинстве случаев его реализация не должна вызывать затруднений.
Таким образом, вся обработка пользовательских данных основана на инфраструктуре команд. Это простое и изящное архитектурное решение. Данные пребывают в полях форм, не вызывая какой-либо активности программы, до тех пор, пока пользователь не запускает одну из команд. Эта команда может поместить данные в хранилище фактов, после чего они станут доступны всему приложению, в частности, любым шаблонам. Если команда также ответственна за сохранение данных в постоянном хранилище, данные RDF могут быть синхронизированы с локальным диском или переданы по сети.
16.6.6. Усовершенствование команд для работы с содержимым RDF
Теперь мы обратимся к конкретному коду, который обеспечивает сохранение на диске введенных пользователем данных. Мы усовершенствуем функции action(), вызываемые из панели инструментов и диалогового окна, а также реализуем дополнительный контроллер команды для работы с ключевыми словами в диалоговом окне.
Функция action() панели инструментов поддерживает команды notetaker-open-dialog, notetaker-save, notetaker-display и notetaker-delete. Только команды -save и -delete связаны с изменением содержимого RDF. Реализация обеих команд довольно громоздка, поэтому в листинге 16.15 показан только код, относящийся к более простой команде notetaker-save.
function action(task) { var ns = "http://www.mozilla.org/notetaker-rdf#"; var rdf = Cc["@mozilla.org/rdf/rdf-service;1"]; rdf = rdf.getService(Ci.nsIRDFService); var container = Cc["@mozilla.org/rdf/container;1"]; container = container.getService(Ci.nsIRDFContainer); var url_node; // ... реализация других команд удалена ... if ( task == "notetaker-save" ) { var summary = document.getElementById("notetaker-toolbar.summary"); var keyword = document.getElementById("notetaker-toolbar.keywords"); var update_type = null; if ( note.url != null ) { if ( keyword.value != "" || summary.value != note.summary ) { update_type = "partial"; // существующая заметка: обновить аннотацию, ключевые слова url_node = rdf.GetResource(note.url); } } else if ( window.content && window.content.document && window.content.document.visited ) { update_type = "complete"; // a new note url_node = window.content.document.location.href; url_node = url_node.replace(/\?.*/,""); // toolbar chops any query url_node = rdf.GetResource(url_node); } if ( update_type == "complete" ) { // добавить url заметки к контейнеру var note_cont = rdf.GetResource("urn:notetaker:notes"); container.Init(noteSession.datasource,note_cont); container.AppendElement(url_node); // добавить поля заметки, за исключением ключевых слов var names = ["details", "top", "left", "width", "height"]; var prop_node, value_node; for (var i=0; i < names.length; i++) { prop_node = rdf.GetResource(ns + names[i]); value_node = rdf.GetLiteral(note[names[i]]); noteSession.datasource.Assert(url_node, prop_node, value_node, true); } } if ( update_type != null) { // обновить/добавить аннотацию var summary_pred = rdf.GetResource(ns + "summary"); var summary_node = rdf.GetLiteral(summary.value); noteSession.datasource.Assert(url_node, summary_pred, summary_node, true); // начать работу с новым ключевым словом var keyword_node = rdf.GetResource("urn:notetaker:keyword:" + keyword.value); var keyword_value = rdf.GetLiteral(keyword.value); // сделать ключевое слово связанным с одним из ключевых слов для данной заметки var keyword_pred = rdf.GetResource(ns + "keyword"); var related_pred = rdf.GetResource(ns + "related"); var keyword2 = noteSession.datasource.GetTarget(url_node, keyword_pred, true); if (keyword2) noteSession.datasource.Assert(keyword_node, related_pred, keyword2, true); // добавить ключевое слово к данной заметке noteSession.datasource.Assert(url_node, keyword_pred, keyword_node, true); // добавить текст ключевого слова var label_pred = rdf.GetResource(ns + "label"); noteSession.datasource.Assert(keyword_node, label_pred, keyword_value, true); // добавить ключевое слово к контейнеру, содержащему все ключевые слова var keyword_cont = rdf.GetResource("urn:notetaker:keywords"); container.Init(noteSession.datasource,keyword_cont); container.AppendElement(keyword_node); } // записать на диск noteSession.datasource.QueryInterface(Ci.nsIRDFRemoteDataSource) .Flush(); note.resolve(); display_note(); }Листинг 16.15. Обновление и сохранение данных RDF в приложении NoteTaker
Этот код содержит эквивалент одной транзакции применительно к данным RDF. Вначале выполняются стандартные подготовительные действия – инициализация интерфейсов XPCOM. Затем код обращается к пользовательскому интерфейсу, чтобы решить, какой тип сохранения необходим в данной ситуации. На основе содержимого полей аннотации и ключевых слов, состояния объектов note и noteSession, а также состояния текущего URL принимается решение о том, существует ли уже заметка для данного URL. Чтобы облегчить себе задачу, мы используем некоторые данные, полученные функцией content_poll(), например значение свойства visited.
Если заметка уже существует, сохранение подразумевает частичное обновление уже существующих фактов. Если же заметки для данного URL еще нет, необходимо создать ее, что подразумевает создание всех необходимых фактов. Если новая заметка создается при помощи панели инструментов, мы удаляем все параметры запроса GET из URL. Значение, указывающее на характер необходимого обновления, присваивается переменной update_type.
Поскольку частичное обновление представляет собой подмножество полного обновления (создания новой заметки), существует часть кода, которая выполняется при любом обновлении. Ветвь кода, следующая за оператором:
if ( update_type == "complete" )
содержит действия, необходимые для полного обновления, за исключением общей части. Общая часть содержится в следующей ветви, которая выполняется при любом типе обновления:
if (update_type != null )
Рассмотрим каждую из ветвей поочередно.
При создании новой заметки сначала мы получаем доступ к контейнеру urn:notetaker:notes, а затем добавляем к нему URL, соответствующий заметке. Затем создаются факты для каждого из свойств заметки, кроме аннотации и ключевых слов. Любые строки перед передачей интерфейсам RDF должны быть преобразованы к типу nsIRDFNote или его подтипам. Всего создается шесть новых фактов (один для URL и пять для свойств заметки).
Затем вызывается код для частичного обновления. Он выполняется всегда за исключением случаев, когда новую заметку создать нельзя. Заметку невозможно создать, например, для URL about:blank или FTP-сайта; в этом случае команда notetaker-save не приведет ни к каким действиям. Для сохранения аннотации мы просто добавляем еще один факт. Для добавления мы используем метод Assert(), который не выполняет никаких действий, если такой факт уже существует. Поскольку в нормальных условиях в хранилище не должно существовать несколько копий одного и того же факта, для добавлении новых фактов рекомендуется использовать этот метод.
Затем код выполняет более сложную работу по добавлению ключевого слова. Если заметке уже были присвоены ключевые слова, мы хотим, чтобы новое слово было связано с остальными. Для этого мы должны добавить факт, связывающий новое слово с одним из ранее присвоенных слов. Для этого мы пытаемся найти ключевое слово, уже присвоенное данной заметке, и в случае успеха добавляем факт, связывающий его с новым словом. Мы делаем это перед тем, как связать новое ключевое слово с заметкой, чтобы избежать ситуации, в которой новое слово окажется связанным с самим собой. Логика оставшегося кода прямолинейна – мы добавляем ключевое слово к заметке, к контейнеру ключевых слов urn:notetaker:keywords и, наконец, сохраняем само слово (его значение).
Таким образом, в этой ветви кода мы добавили пять фактов – один для аннотации и четыре для ключевых слов. Поскольку источник данных основан на полноценном xml-datasource, эти изменения будут автоматически переданы всем шаблонам, использующим данный источник. Затем мы вызываем метод Flush(), чтобы записать состояние источника данных на локальный диск. Имейте в виду, что этот метод полностью переписывает файл notetaker.rdf практически в произвольном порядке, и любое форматирование файла будет утрачено. Наконец, мы обновляем объект note и отображаем данные заметки, чтобы привести структуры данных JavaScript и пользовательский интерфейс в соответствие с данными RDF. На этом мы заканчиваем обсуждение реализации команды notetaker-save.
Команда notetaker-delete реализуется аналогичным образом. Наибольшую сложность при этом представляет определение того, какие из ключевых слов, связанных с заметкой, могут быть удалены, а какие нужны для других заметок. Это требует анализа многих возможных вариантов, и мы не будем рассматривать их здесь. С кнопкой Delete (Удалить) на панели Keywords связана аналогичная логика; мы обсудим ее ниже.
Функция action() диалогового окна Edit поддерживает следующие команды: notetaker-nav-edit, notetaker-nav-keywords, notetaker-save, notetaker-load и notetaker-close-dialog. Из них лишь команда notetaker-save требует работы с данными RDF. Как показано в листинге 16.16, для ее реализации мы можем использовать команду notetaker-save панели инструментов.
if (task == "notetaker-save") { var field, widget, note = window.opener.note; for (field in note) { widget = document.getElementById("dialog." + field.replace(/_/,"-")); if (!widget) continue; if (widget.tagName == "checkbox") note[field] = widget.checked; else note[field] = widget.value; } window.opener.setTimeout('execute("notetaker-save")',1); }Листинг 16.16. Усовершенствованная команда notetaker-save для диалогового окна.
Последняя инструкция этого фрагмента представляет собой вызов команды notetaker-save панели инструментов. Мы не можем вызвать метод window.opener.execute() непосредственно, поскольку в этом случае функция будет выполняться в контексте диалогового окна, а не окна браузера. Обращаясь к методу setTimeout() окна браузера, мы обеспечиваем необходимый контекст выполнения функции.
Наконец, нам нужны дополнительные команды для работы с ключевыми словами в панели Keywords диалогового окна Edit. Эти команды будут объединены в контроллер, поддерживающий принятие или отмену изменений (фиксацию и откат транзакций). Контроллер будет поддерживать следующие команды: notetakerkeyword-add, notetaker-keyword-delete, notetaker-keyword-commit и notetaker-keyword-undo-all. Поскольку эти команды тесно связаны друг с другом и используют общие данные, их нецелесообразно реализовывать по отдельности в составе функции action(). Вместо этого мы реализуем их непосредственно в составе контроллера, создав специально для этого новый файл keywordController.js. Общая структура контроллера показана в листинге 16.17.
var keywordController = { _cmds : { }, _undo_stack : [], _rdf : null, _ds : null, _ns : "http://www.mozilla.org/notetaker-rdf#", _related : null, _label : null, _keyword : null, init : function (ds) { ... initialize ... }, _LoggedAssert : function (sub, pred, obj) { ... }, _LoggedUnassert : function (sub, pred, obj) { ... }, supportsCommand : function (cmd) { return (cmd in this._cmds); }, isCommandEnabled : function (cmd) { return true; }, onEvent : function (cmd) { return true; }, doCommand : function (cmd) { ... подготовительные операции ... switch (cmd) { case "notetaker-keyword-add": case "notetaker-keyword-delete": case "notetaker-keyword-commit": case "notetaker-keyword-undo-all": } } }; keywordController.init(window.opener.noteSession.datasource);Листинг 16.17. Контроллер команд для работы с ключевыми словами в диалоговом окне
Как и любой контроллер команд, этот контроллер поддерживает четыре стандартных метода, начиная с supportsCommand(). Метод doCommand() выполняет различные действия в зависимости от переданного имени команды; код в нем организован при помощи оператора case. Контроллер также поддерживает ряд других действий. В массиве _undo_stack сохраняются действия, которые могут быть отменены. Реализованные нами методы _LoggedAssert() и _LoggedUnassert() аналогичны стандартным методам для работы с RDF Assert() и Unassert(), однако кроме этого они выполняют журналирование – оставляют запись о выполненных действиях в массиве _undo_stack. Давайте начнем анализ контроллера с метода init(), который показан в листинге 16.18:
init : function (ds) { this._rdf = Cc["@mozilla.org/rdf/rdf-service;1"]; this._rdf = this._rdf.getService(Ci.nsIRDFService); this._ds = ds; this._related = this._rdf.GetResource(this._ns + "related"); this._label = this._rdf.GetResource(this._ns + "label"); this._keyword = this._rdf.GetResource(this._ns + "keyword"); window.controllers.insertControllerAt(0,this); },Листинг 16.18. Инициализация контроллера команд для работы с ключевыми словами.
Этот метод создает доступные для контроллера ссылки на ряд полезных объектов – службу RDF, источник данных и три часто используемых предиката фактов. Затем контроллер регистрируется в диалоговом окне. Поскольку он оказывается первым в цепочке контроллеров, мы можем быть уверены, что любая команда, поддерживаемая этим контроллером, будет обработана именно им.
Реализация двух следующих функций – _LoggedAssert() и _LoggedUnassert() – иллюстрирует, каким образом контроллер может сохранять информацию о выполняемых командах. В данном случае речь идет об истории добавления и отмены фактов RDF, которая в дальнейшем может использоваться для отмены выполненных действий.
Реализация двух этих функций показана в листинге 16.19.
_LoggedAssert : function (sub, pred, obj) { if ( !this._ds.HasAssertion(sub, pred, obj, true)) { this._undo_stack.push( { assert:true, sterm:sub, pterm:pred, oterm:obj } ); this._ds.Assert(sub, pred, obj, true); } }, _LoggedUnassert : function (sub, pred, obj) { if ( this._ds.HasAssertion(sub, pred, obj, true)) { this._undo_stack.push( { assert:false, sterm:sub, pterm:pred, oterm:obj } ); this._ds.Unassert(sub, pred, obj, true); } },Листинг 16.19. Журналирование добавления и удаления фактов
Эти функции представляют собой замену стандартных методов nsIRDFDataSource.Assert() и nsIRDFDataSource.Unassert(). В обоих случаях сначала выполняется проверка того, изменит ли предполагаемое действие состояние хранилища фактов. Если это так, создается запись о действии в форме объекта с четырьмя свойствами, которая добавляется в журнал (стек). Свойство assert указывает на характер выполняемого действия. После этого вызывается стандартный метод для изменения фактов RDF.
Журнал изменений, создаваемый при помощи этих двух функций, может использоваться командами notetaker-keyword-commit и notetaker-keyword-undo-all. Реализация этих команд в составе метода doCommand() представлена в листинге 16.20.
case "notetaker-keyword-commit": this._undo_stack = []; break; case "notetaker-keyword-undo-all": while (this._undo_stack.length > 0 ) { var cmd = this._undo_stack.pop(); if ( cmd.assert ) this._ds.Unassert(cmd.sterm, cmd.pterm, cmd.oterm, true); else this._ds.Assert(cmd.sterm, cmd.pterm, cmd.oterm, true); } break;Листинг 16.20. Принятие и отмена изменений с использованием журнала
Реализация команды notetaker-keyword-commit тривиальна – она просто удаляет из журнала все записи, чтобы выполненные действия нельзя было отменить в дальнейшем. Команда notetaker-keyword-undo-all чуть более сложна. Она перебирает элементы стека, выполняя Unassert() для каждого Assert() 'а, записанного в журнале, и наоборот. В конце концов стек оказывается пустым, так что "отмена отмены" в данной реализации невозможна.
Хотя в данном случае в журнал записываются добавления и удаления отдельных фактов, столь же просто организовать журналирование целых команд. Эта возможность обсуждалась в "Команды" "Команды".
Оставшаяся часть метода doCommand() приведена в листинге 16.21.
doCommand : function (cmd) { var url = window.opener.content.document.location.href; var keyword = window.document.getElementById("dialog.keyword").value; if (keyword.match(/^[ \t]*$/)) return; var keyword_node = this._rdf.GetResource("urn:notetaker:keyword:" + keyword); var keyword_value = this._rdf.GetLiteral(keyword); var url_node = this._rdf.GetResource(url); var test_node, keyword2, enum1, enum2; switch (cmd) { case "notetaker-keyword-add": // Связать ключевое слово с другим, если таковое существует keyword2 = this._ds.GetTarget(url_node, this._keyword, true); if (keyword2) this._LoggedAssert(keyword_node, this._related, keyword2); // добавить данное ключевое слово this._LoggedAssert(keyword_node, this._label, keyword_value); // добавить ключевое слово к текущей заметке this._LoggedAssert(url_node, this._keyword, keyword_node); break; case "notetaker-keyword-delete": // удалить связь ключевого слова с текущей заметкой. this._LoggedUnassert(url_node, this._keyword, keyword_node); // если ключевое слово не используется в других местах, удалить его и связанные с ним факты enum1 = this._ds.GetSources( this._keyword, keyword_node, true); if (!enum1.hasMoreElements()) { // данное ключевое слово this._LoggedUnassert(keyword_node, this._label, keyword_value); // ключевое слово, с которым связано данное слово enum2 = this._ds.GetTargets(keyword_node, this._related, true); while (enum2.hasMoreElements()) this._LoggedUnassert(keyword_node, this._related, enum2.getNext().QueryInterface(Ci.nsIRDFNode)); // ключевое слово, которое связано с данным словом enum2 = this._ds.GetSources(this._related, keyword_node, true); while (enum2.hasMoreElements()) this._LoggedUnassert(enum2.getNext().QueryInterface(Ci.nsIRDFNode), this._related, keyword_node); } else // ключевое слово используется в других местах { // удалить факты, если слова, с которыми связано данное ключевое слово, относятся только к данной заметке enum1 = this._ds.GetTargets(keyword_node, this._related, true); while (enum1.hasMoreElements()) { keyword2 = enum1.getNext().QueryInterface(Ci.nsIRDFNode); enum2 = this._ds.GetSources(this._keyword, keyword2, true); test_node = enum2.getNext().QueryInterface(Ci.nsIRDFNode); if (!enum2.hasMoreElements() && test_node.EqualsNode(url_node)) this._LoggedUnassert(keyword_node, this._related, keyword2); // удалить факты, если слова, которые связаны с данным ключевым словом, относятся только к данной заметке enum1 = this._ds.GetSources(this._related, keyword_node, true); while (enum1.hasMoreElements()) { keyword2 = enum1.getNext().QueryInterface(Ci.nsIRDFNode); enum2 = this._ds.GetSources(this._keyword, keyword2, true); test_node = enum2.getNext().QueryInterface(Ci.nsIRDFNode); if (!enum2.hasMoreElements() && test_node.EqualsNode(url_node)) this._LoggedUnassert(keyword2, this._related, keyword_node); } } } break;Листинг 16.21. Инициализация метода doCommand(), добавление и удаление ключевых слов
Около десятка строк, находящихся до оператора switch(), выполняют инициализацию локальных переменных и прекращают выполнение команды в том случае, если текущая заметка отсутствует. В листинге показаны только ветви оператора switch(), соответствующие командам notetaker-keyword-add и notetaker-keyword-delete. Добавление и удаление ключевых слов было бы проще, если бы мы не пытались поддерживать логику связей между ключевыми словами. Значительная часть кода, особенно для удаления ключевых слов, связана с решением этой задачи.
Обе команды предполагают, что заметка для данного URL уже существует или создается в данный момент. Поэтому ключевые слова добавляются и удаляются в контексте определенного URL. Основные действия отмечены в коде при помощи комментариев, однако ниже приводятся более подробные пояснения.
Весь код написан с учетом того ограничения, что в хранилище фактов не могут находиться две копии одного и того же факта. Каждый факт в хранилище уникален. Поэтому мы должны работать с хранилищем глобально, учитывая последствия добавления или удаления отдельных фактов для хранилища в целом.
Добавление факта – более простой случай, чем удаление. Необходимо добавить к хранилищу следующие факты:
<- keyword-urn, related, keyword2-urn -> (не обязательно) <- note-url, keyword, keyword-urn -> <- keyword-urn, label, keyword-literal ->
Мы должны сделать так, чтобы все ключевые слова, присвоенные одной заметке, были связаны между собой. В нашей модели RDF для этого достаточно связать добавляемое слово хотя бы с одним из ключевых слов, уже присвоенных заметке. После этого вся совокупность слов, связанных между собой, может быть установлена путем перемещения по связям. Поэтому сначала мы проверяем наличие ключевых слов у данной заметки и, если таковые обнаружены, связываем новое слово с одним из них. После этого мы можем создать факты, отражающие принадлежность нового слова текущей заметке, а также значение (текст) этого слова. Каждый из фактов создается при помощи метода _LoggedAssert().
Процедура удаления ключевых слов значительно более сложна. Нетрудно удалить информацию, связанную с конкретной заметкой, но ключевое слово может быть присвоено нескольким заметкам. Это означает, что оно может быть включено в связи между ключевыми словами, также относящиеся к другим заметкам. Поэтому мы не можем просто удалить факты первого и третьего типа, не изучив вопрос, где еще используется данное ключевое слово. Наши дальнейшие действия будут зависеть от того, присвоено ли оно другим заметкам. Это требует довольно громоздкой логики, которая описана ниже.
Если ключевое слово используется для единственной заметки, то вся информация об этом слове ограничена данной заметкой. Поэтому мы можем просто удалить все три факта.
Если же слово используется для других заметок, следует действовать более аккуратно. Мы можем удалить факт о связи ключевого слова с данной заметкой, но мы не можем удалить само ключевое слово (факт о его значении). Самая сложная часть логики – удаление фактов, описывающих связь данного слова с другими ключевыми словами. Если факт о связи данного ключевого слова с другим сформирован на основе другой заметки, мы не должны удалять его. Поэтому, если другое слово присвоено какой-либо другой заметке наряду с данным, факт относится и к этой заметке и потому должен быть сохранен. В противном случае следует удалить этот факт. Мы выполняем проверку дважды, поскольку удаляемое ключевое слово может быть как субъектом, так и объектом факта о связи ключевых слов.
Внимательный читатель может заметить, что контейнер urn:notetaker:keywords также должен быть обновлен командами -add и -delete. Мы не сделали этого, поскольку реализация команд и без того достаточно сложна, а для того, чтобы увязать дальнейшие обновления с нашей системой отмены, требуются определенные ухищрения. Более общим решением было бы связать объекты- наблюдатели источника данных со стеком-журналом, однако здесь мы не будем рассматривать это решение. На этом мы завершаем обсуждение команд NoteTaker для работы с данными RDF.
Для того чтобы эта система заработала, необходимо связать контроллер и команды с диалоговым окном. Для этого требуется несколько фрагментов кода. Мы должны добавить к файлу editDialog.xul дополнительный тег <script>:
Кроме того, мы должны добавить несколько обработчиков событий к тегу <dialog> в том же файле. Эти обработчики нужны для работы с ключевыми словами из диалогового окна.
<dialog xmlns="http://www.mozilla.org/keymaster/ gatekeeper/there.is.only.xul" id="notetaker.dialog" title="Edit NoteTaker Note" onload="execute('notetaker-load');" ondialogaccept="execute('notetaker-keyword-commit'); execute('notetaker-save'); execute('notetaker-close-dialog');" ondialogcancel="execute('notetaker-keyword-undo-all'); execute('notetaker-close-dialog');" >
Обработчики разрастаются, и если по мере развития приложения придется добавлять новые, целесообразно объединить группы команд в функции. Некоторые другие обработчики для диалогового окна определены в файле dialog_handlers.js. Теперь, когда мы создали контроллер для работы с ключевыми словами, два из этих обработчиков превращаются в тривиальные вызовы команд:
function add_click(ev) { execute("notetaker-keyword-add"); } function delete_click(ev) { execute("notetaker-keyword-delete"); }
Теперь мы можем считать, что приложение NoteTaker завершено – по крайней мере, в той степени, в какой это позволяет сделать объем книги.
16.6.7. Кадры (views) деревьев RDF и источники данных.
В практическом разделе "Списки и Деревья" "Списки и деревья" мы экспериментировали с "кадрами". При желании этот эксперимент может быть распространен на данные RDF. В таком случае тег <tree>, содержащий кадр, может получать содержимое из источника данных без помощи шаблона. Поскольку объем книги не позволяет подробно обсудить этот вопрос, сделаем несколько кратких замечаний.
В "Списки и Деревья" мы реализовали метод calcRelatedMatrix(), который получал данные из массива treedata. Мы можем изменить этот метод так, чтобы он получал связанные пары ключевых слов не из массива JavaScript, а от источника данных. В этом случае наш код заработает немедленно, но уже на основе данных RDF.
Однако такая стратегия – довольно примитивное использование возможностей источников данных. Более разумное решение – использовать интерфейс nsIRDFObserver. Если объект JavaScript, реализующий кадр, поддерживает этот интерфейс, он может быть зарегистрирован в качестве наблюдателя в источнике данных (источник, в свою очередь, должен поддерживать интерфейс nsIRDFCompositeDataSource ). В результате кадр будет получать уведомление всякий раз, когда факт в источнике изменяется, и сможет отразить это изменение в элементе <tree>, не перестраивая дерево полностью. Эта более сложная стратегия совместима с системами управления событиями, в которых события могут поступать с удаленного сервера.
В заключение отметим, что объект с интерфейсом nsIRDFDataSource может быть полностью реализован на JavaScript. Такой объект может имитировать источник данных RDF. С другой стороны, в него могут быть "завернуты" один или несколько источников данных, так что фактически он будет выступать в качестве составного источника. В любом случае, такой объект может быть подключен к шаблону точно так же, как и источники, предоставляемые платформой.
16.7. Отладка: работа с источниками данных
Вот некоторые из типичных проблем, возникающих при работе с источниками данных:
Регистр символов. В отличие от остальных методов XPCOM, имена методов интерфейсов источников данных начинаются с прописной буквы – InitCaps, а не initCaps. Поэтому, например, метод называется GetResource(), а не getResource().
Асинхронная загрузка. Если для создания источника данных используется метод GetDataSource() интерфейса nsIRDFDataSource, а не GetDataSourceBlocking(), данные загружаются в источник параллельно с выполнением других инструкций. В результате попытка обратиться к данным немедленно после создания источника может привести к ошибке. Данные, заведомо присутствующие в источнике с точки зрения разработчика, могут оказаться еще не загружены.
Синтаксические ошибки в тестовых данных. Тестовые данные из файла RDF, содержащего синтаксические ошибки, будут загружены лишь до места первой ошибки. При этом сообщение об ошибке выдано не будет.
Попытка сохранить данные по сети. Если вы вносите изменения в источники, получающие данные из сети (с Web-ресурса или FTP-сайта), такие изменения не могут быть непосредственно "сохранены". Этот механизм применим только к локальным файлам. Чтобы передать сделанные изменения на удаленный сервер, необходимо создать на основе источника данных документ RDF, используя интерфейс источника содержимого. Затем полученный документ можно загрузить на сервер при помощи механизма передачи файлов. Решение более низкого уровня может быть основано на использовании сокета.
Использование false в качестве аргумента методов Assert() и Unassert(). Четвертым аргументом этих методов всегда должно быть true. Использование false расширяет логику методов RDF непродуктивным образом. Всегда используйте true.
Передача строк методам Assert() и Unassert(). Эти методы принимают только объекты типа nsIRDFNode и его подтипов.
Проблемы с множественными возвращаемыми значениями. Такие методы, как GetTargets() возвращают объект-перечислитель nsISimpleEnumerator, содержащий список URI, соответствующих запросу. Все объекты, возвращаемые перечислителем, поддерживают интерфейс nsISupports. Используйте метод QueryInterface() для того, чтобы получить более полезный интерфейс nsIRDFNode или один из его подтипов.
Объекты, реализующие интерфейс nsIRDFContainerUtils, являются служебными. Они используются для выполнения различных действий с другими объектами, передаваемыми им в качестве аргументов.
16.7.1. Получение содержимого источника данных
Внутренние источники данных RDF являются одним из наиболее сложных аспектов платформы Mozilla. В листинге 16.22 представлен фрагмент кода, который может использоваться для получения их содержимого.
function _dumpFactSubtree(ds, sub, level) { var iter, iter2, pred, obj, objstr, result=""; // выйти, если передан nsIRDFLiteral или другой не-URI try { iter = ds.ArcLabelsOut(sub); } catch (ex) { return; } while (iter.hasMoreElements()) { pred = iter.getNext().QueryInterface(Ci.nsIRDFResource); iter2 = ds.GetTargets(sub, pred, true); while (iter2.hasMoreElements()) { obj = iter2.getNext(); try { obj = obj.QueryInterface(Ci.nsIRDFResource); objstr = obj.Value; } catch (ex) { obj = obj.QueryInterface(Ci.nsIRDFLiteral); objstr = '"' + obj.Value + '"'; } result += level + " " + sub.Value + " , " + pred.Value + " , " + objstr + "\n"; result += dumpFactSubtree(ds, obj, level+1); } } return result; } function dumpFromRoot(ds, rootURI) { return _dumpFactSubtree(ds, rootURI, 0); }Листинг 16.22. Получение содержимого источника данных.
Для вывода всех данных источника следует вызвать функцию dumpFromRoot(). Этот код использует ограниченное подмножество функциональности интерфейса nsIRDFDataSource и должен работать с большинством внутренних источников данных, а также с простыми файлами RDF. Код выполняет рекурсивный поиск, начиная с указанного узла, и исходит из того, что граф RDF в источнике данных организован как дерево.
В качестве аргументов функции должны быть переданы объекты nsIRDFDataSource и nsIRDFResource. Источник данных, представленный первым объектом, должен быть полностью загружен – в противном случае может быть выведена неполная информация о его содержимом. Аргумент rootURI должен быть контейнером или владельцем контейнера на графе RDF источника данных. В качестве примера можно привести URI, представленные в таблице 16.11. Если граф RDF содержит циклы, код будет выполнять бесконечную рекурсию, что может привести к зависанию платформы или аварийному прекращению ее работы. Это довольно примитивный инструмент для тестирования, так что следует относиться к нему соответствующим образом.
16.8. Итоги
Инфраструктура платформы Mozilla содержит множество полезных объектов, не все из которых были освещены в этой лекции. Большинство из них являются объектами довольно высокого уровня в силу требований переносимости и ориентации на разработку приложений. Можно представить себе, что в дальнейшем в составе платформы будет реализован аналог интерфейса POSIX, однако в силу общей ориентированности платформы на разработку приложений высокого уровня потребность в таком интерфейсе невелика.
Mozilla предоставляет богатые возможности для работы с XML, что неудивительно. Первоначально интенсивная обработка XML была характерна для приложений класса business-to-business, однако инициатива .NET компании Microsoft подразумевает активное использование XML на стороне клиента.
К настоящему моменту мы рассмотрели как интерфейс приложений Mozilla, так и их инфраструктуру, и нам осталось лишь познакомиться с развертыванием этих приложений. Наряду с системой сборки Mozilla, которая используется для создания компилированных приложений, существует и система удаленной установки приложений Mozilla. Эта система, XPInstall, и является предметом "Система распространения и установки - XPInstall" курса.