пятница, 11 декабря 2009 г.

Небольшое введение в механизм точек расширения Eclipse


Здравствуйте, уважаемые читатели. Суровый челябинский программист вернулся из земли Египетской, акклиматизировался и готов порадовать вас новой статьей (хотя, конечно, переход от +28 к - 20 это жестоко).

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

Суть заключается в следующем: в корне плагина находится файл plugin.xml, в котором содержатся объявления точек расширения (тег extension-point) - элементов описывающих те места системы, которые можно расширять. В платформе Eclipse RCP определены точки расширения для регистрации своих адаптеров, видов, перспектив, пунктов меню, кнопочек на панели быстрого запуска и т.д. и т.п.

Точка расширения характеризуется уникальным идентификатором (атрибут id, который обычно задают, придерживаясь доменной модели, как и java-пакеты; именем (атрибут name), служащим больше для удобства программиста, и схемой (атрибут scheme) - путем к exsd-файлу, который содержит XScheme, описывающую формат расширения.

Само расширение описывается внутри тега extension, который содержит xml-код, удовлетворяющий схеме, заданной при создании точки расширения. Т.е. если абстрагироваться от деталей, то в бандле, предоставляющем какой-то расширяемый механизм, мы описываем точку расширения (extension-point), а в других бандлах (которых естественно может быть - несколько) - расширения (extension).


Давайте рассмотрим на примере создание, описание и использование точек расширения. Напишем набор плагинов, добавляющих в меню Eclipse пункт, при выборе которого будет показано диалоговое окно со списком пицц. Каждая пицца характеризуется названием и стоимостью. Зарегистрируем точку расширения name.samolisov.plugins.pizza, позволяющую задавать название пиццы и ее цену. Цена так же может вычисляться динамически - тогда в точку расширения будет передаваться имя класса, реализующего интерфейс ICosteable.

Eclipse Plugin Development Environment (PDE) содержит удобные средства, облегчающие работу по созданию точек расширения, расширений и редактированию exsd-файлов.

Чтобы создать точку расширения, необходимо открыть манифест бандла и перейти на вкладку Extension Points. После нажатия кнопки Add станет доступно диалоговое окно создания точки расширения. Здесь все довольно просто, поэтому подробно останавливаться не будем.



После добавления точки расширения будет создан файл plugin.xml с таким содержанием:

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

  2. <?eclipse version="3.4"?>

  3. <plugin>  

  4.    <extension-point id="name.samolisov.plugins.pizza" name="Pizza" schema="schema/pizza.exsd" />

  5. </plugin>



В PDE же откроется редактор exsd-файла. На вкладке Overview можно задать описание точки расширения (Description), номер первого релиза вашего плагина, в котором была создана ваша точка расширения (Since), примеры ее использования (Examples), информацию об используемом/необходимом API (API Information), информацию о поставляемой реализации (Supplied Implementation) и конечно же информацию о себе любимом (Copyright).




Более важной для функционирования механизма является вкладка Definition, на которой непосредственно задается XScheme, описывающая точку расширения.



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



После того, как мы задали схему - можно определять свои расширения. Делается это на вкладке Extensions редактора манифеста бандла. После нажатия на кнопку Add появится диалоговое окно выбора точки расширения. Если убрать галочку Show only extension points from the reguired plug-ins, то можно будет выбрать любую доступную точку расширения, бандл, в котором она описана, добавится в Required-Bundles автоматически.



После нажатия кнопки Finish, точка расширения добавиться в All Extensions и будет создано соответствующее ей расширение. В правой части редактора будет сгенерирована форма Extension Element Details, соответствующая заданной схеме. Так как мы указали, что наша точка расширения name.samolisov.plugins.pizza описывается последовательностью тегов pizza, каждый из которых характеризуется набором атрибутов cost, name, class, то форма выглядит следующим образом:



Если мы хотим добавить новую пиццу, то нам нужно выбрать New->pizza в контекстном меню точки расширения

После всех манипуляций в файл plugin.xml будут добавлены следующие строчки:

  1. <extension point="name.samolisov.plugins.pizza">

  2.       <pizza cost="100" name="Pepperoni Pizza" />

  3.       <pizza cost="200" name="Alsatian Tarte flambée" />

  4.       <pizza cost="210.25" name="Anatolian Lahmacun" />

  5. </extension>



Теперь нам нужно написать код, который будет извлекать все пиццы из расширений и создавать список бинов Pizza. Код бина Pizza при этом тривиален:

  1. package name.samolisov.plugins.pizza.processor;

  2.  

  3. public class Pizza

  4. {

  5.     private String name;

  6.  

  7.     private float cost;

  8.  

  9.     public Pizza(String name, float cost)

  10.     {

  11.         this.name = name;

  12.         this.cost = cost;

  13.     }

  14.  

  15.     public String getName()

  16.     {

  17.         return name;

  18.     }

  19.  

  20.     public void setName(String name)

  21.     {

  22.         this.name = name;

  23.     }

  24.  

  25.     public float getCost()

  26.     {

  27.         return cost;

  28.     }

  29.  

  30.     public void setCost(float cost)

  31.     {

  32.         this.cost = cost;

  33.     }

  34. }

  35.  



Все точки расширения и сами расширения после регистрации плагина в OSGi попадают в реестр точек расширения Eclipse. Реестр точек расширения - класс, реализующий интерфейс IExtensionRegistry. Реализация данного класса содержится в бандле org.eclipse.equinox.registry. Реестр позволяет получать зарегистрированные в нем точки расширения, добавлять новые (прямо в рантайме), регистрировать листенеры и т.д.

В данной статье мы рассмотрим только получение интересующих нас расширений из реестра:

  1. package name.samolisov.plugins.pizza.processor;

  2.  

  3. import java.util.ArrayList;

  4. import java.util.List;

  5.  

  6. import name.samolisov.plugins.pizza.cost.ICosteable;

  7.  

  8. import org.eclipse.core.runtime.CoreException;

  9. import org.eclipse.core.runtime.IConfigurationElement;

  10. import org.eclipse.core.runtime.IExtensionRegistry;

  11. import org.eclipse.core.runtime.Platform;

  12.  

  13. public class PizzaProcessor

  14. {

  15.     private static final String CLASS = "class";

  16.     private static final String COST = "cost";

  17.     private static final String NAME = "name";

  18.     private static final String PIZZA_EXTENSION_ID = "name.samolisov.plugins.pizza";

  19.  

  20.     public static List<Pizza> getAllPizzas()

  21.     {

  22.         List<Pizza> result = new ArrayList<Pizza>();

  23.  

  24.         IExtensionRegistry reg = Platform.getExtensionRegistry();

  25.         IConfigurationElement[] extensions = reg.getConfigurationElementsFor(PIZZA_EXTENSION_ID);

  26.  

  27.         for (IConfigurationElement extension : extensions)

  28.         {

  29.             result.add(new Pizza(extension.getAttribute(NAME), getCost(extension)));

  30.         }

  31.  

  32.         return result;

  33.     }

  34.  

  35.     private static float getCost(IConfigurationElement extension)

  36.     {

  37.         if (extension.getAttribute(COST) != null)

  38.         {

  39.             return Float.parseFloat(extension.getAttribute(COST));

  40.         }

  41.         else

  42.         {

  43.             try

  44.             {

  45.                 ICosteable costeable = (ICosteable) extension.createExecutableExtension(CLASS);

  46.                 return costeable.getCost();

  47.             }

  48.             catch (CoreException e)

  49.             {

  50.                 e.printStackTrace();

  51.                 return 0;

  52.             }

  53.         }

  54.     }

  55. }



В данном примере мы получаем все расширения name.samolisov.plugins.pizza и формируем список бинов Pizza. Если цена пиццы не задана в атрибуте cost, то получаем экземпляр класса, определенного в атрибуте class с помощью вызова IConfigurationElement#createExecutableExtension(CLASS) и вызываем определенный в нем метод getCost().

Теперь рассмотрим создание пункта меню Pizza, при выборе которого будет показано диалоговое окно со списком пицц. Создаем бандл name.samolisov.plugins.pizza.ui. Активатор этого бандла должен наследоваться от класса AbstractUIPlugin. Прежде всего необходимо создать действие - класс, реализующий интерфейс IWorkbenchWindowActionDelegate, в методе run которого определяется логика действия:

  1. package name.samolisov.plugins.pizza.ui.actions;

  2.  

  3. import java.util.List;

  4.  

  5. import name.samolisov.plugins.pizza.processor.Pizza;

  6. import name.samolisov.plugins.pizza.processor.PizzaProcessor;

  7.  

  8. import org.eclipse.jface.action.IAction;

  9. import org.eclipse.jface.dialogs.MessageDialog;

  10. import org.eclipse.jface.viewers.ISelection;

  11. import org.eclipse.ui.IWorkbenchWindow;

  12. import org.eclipse.ui.IWorkbenchWindowActionDelegate;

  13.  

  14. public class PizzaSampleAction implements IWorkbenchWindowActionDelegate

  15. {

  16.     private IWorkbenchWindow window;

  17.  

  18.     public void dispose()

  19.     {

  20.     }

  21.  

  22.     public void init(IWorkbenchWindow window)

  23.     {

  24.         this.window = window;

  25.     }

  26.  

  27.     public void run(IAction action)

  28.     {

  29.         List<Pizza> list = PizzaProcessor.getAllPizzas();

  30.         StringBuffer buffer = new StringBuffer();

  31.  

  32.         for (Pizza pizza : list)

  33.             buffer.append(pizza.getName()).append(" (").append(pizza.getCost()).append(")\n");

  34.  

  35.         MessageDialog.openInformation(window.getShell(), "Installed pizza toppings", buffer.toString());

  36.     }

  37.  

  38.     public void selectionChanged(IAction action, ISelection selection)

  39.     {

  40.     }

  41. }

  42.  



В нашем случае логика довольно проста: с помощью рассмотренного выше класса PizzaProcessor мы получаем список Pizza, зарегистрированных в системе и формируем их них текст, выводимый в окне (каждая строчка - информация об одной пицце в формате "название (цена)").

Теперь данное действие нужно зарегистрировать. Для этого существует точка расширения org.eclipse.ui.actionSets, описывающая множество действий. Нам необходимо создать свое множество действий (тег actionSet), характеризующееся меткой (атрибут label), признаком видимости (visible) и идентификатором (id). Внутри множества действий описываются сами действия и пунты меню. Действие (тег action) характеризуется иконкой, меткой, всплывающей подсказкой, классом, путем к связанному с действием пункту меню, путем к кнопке на панели быстрого запуска и идентификатором. Меню (тег menu) характеризуется меткой, идентификатором и иерархией пунктов меню. В коде наше расширение выглядит следующем образом:

  1.     <extension point="org.eclipse.ui.actionSets">

  2.         <actionSet label="Sample Action Set" visible="true"

  3.                   id="name.samolisov.plugins.pizza.ui.actionSet">

  4.             <action icon="icons/sample.gif" label="&amp;Pizza" tooltip="Pizza"

  5.                    class="name.samolisov.plugins.pizza.ui.actions.PizzaSampleAction"

  6.                    menubarPath="name.samolisov.plugins.pizza.ui.menu.PizzaSample/sampleGroup"

  7.                    toolbarPath="sampleGroup"

  8.                    id="name.samolisov.plugins.pizza.ui.actions.PizzaSample" />    

  9.            

  10.             <menu id="name.samolisov.plugins.pizza.ui.menu.PizzaSample" label="Pizza">

  11.                 <separator name="sampleGroup" />

  12.             </menu>                          

  13.         </actionSet>

  14.     </extension>



Запускаем наши бандлы как Eclipse Application - появляется окно эклипса в котором присутствует пункт меню Pizza:



Если выбрать данный пункт меню - появится окно со списком пицц



Можно поэкспериментировать - создать новый плагин, можно без активатора. В плагине нужно определить свое расширение для name.samolisov.plugins.pizza, в котором зарегистрировать еще пицц. Сам бандл необходимо добавить в конфигурацию запуска нашего приложения, после чего новые пиццы будут отображаться в окне Installed pizza toppings.



Точки расширения - довольно уже обкатанный и зарекомендовавший себя механизм декларативного (это важно) описания расширяемой функциональности. Другим таким механизмом, появившемся относительно недавно, являются декларативные сервисы. Хорошее исследование на тему того, что лучше можно прочитать в Neil’s point-free blog.

UPD 30.01.10: Точки расширения не являются элементом Eclipse Core, их вполне можно использовать на "голом" Equinox. Для этого необходимо добавить в конфигурацию запуска бандл org.eclipse.equinox.registry и проследить, чтобы он находился в состоянии ACTIVE.

Как обычно вы можете задавать мне вопросы и высказывать свое мнение в комментариях. Буду рад, если данное введение окажется вам полезным.

Скачать примеры к статье (исходники и конфигурации запуска. Rar, 21 Кб)

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

Комментариев нет:

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

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