En un gvSIG instalado, existe un subdirectorio llamado gvSIG/extensiones. Dicho subdirectorio contiene a su vez un conjunto de directorios, cada uno de los cuales constituye un plugin. Los plugins no son más que un conjunto de clases Java que añaden nuevas funcionalidades a la aplicación. Si inspeccionamos el mencionado directorio observaremos que existe un plugin llamado com.iver.core, que contiene libCorePlugin (el skin de Andami) y un plugin llamado com.iver.cit.gvsig, que contiene el plugin gvSIG (el plugin principal de la aplicación). Dependiendo de la instalación de gvSIG que hayamos realizado, podremos localizar otros plugins como com.iver.cit.gvsig.cad (funcionalidades de edición de cartografía), org.gvsig.scripting (que añade las capacidades de scripting), com.iver.cit.gvsig.wms (cliente de WMS), etc
Las nuevas funcionalidades añadidas por los plugins, deben conectarse de alguna forma con el resto de la aplicación. Este punto de conexión lo aportan las extensiones, que son clases Java que implementan la interfaz IExtension, y que hacen de puente entre la funcionalidad ya existente y la nueva funcionalidad aportada por el plugin.
El contenido mínimo que un plugin debe incluir se reduce a un fichero XML llamado config.xml. Este fichero contiene el nombre del plugin y un abanico de detalles relevantes sobre el mismo: el directorio que contendrá las librerías aportadas por el plugin, las dependencias que tiene con otros plugins, el nombre base de los ficheros de traducciones, las extensiones que aporta, y una descripción de los componentes que deben añadirse a la interfaz de usuario (menús, barras de herramientas, etc).
Dependiendo de los elementos definidos en el config.xml, el plugin puede incluir también: uno o varios ficheros JAR en el directorio de librerías, varios ficheros de traducciones, imágenes o cualquier otro tipo de datos (cartografía, datos de conexión a algún servicio, paletas de colores, etc)
Podemos ver la estructura del plugin WFS para hacernos una idea de una distribución típica de ficheros:
gvSIG/ extensiones/ com.iver.cit.gvsig.wfs2/ build.number config.xml text_en.properties text_fr.properties \.\. text.properites lib/ com.iver.cit.gvsig.wfs2.jar images/ backward.png down-arrow.png fastbackward.png fastforward.png
Normalmente, las librerías se depositan en el directorio raíz del plugin, o en un subdirectorio lib/. Las imágenes suelen situarse en un subdirectorio images. Sin embargo, la estructura puede alterarse a nuestro antojo, siempre que en el código usemos la ruta relativa adecuada (en el caso de las imágenes) o definamos correctamente el directorio en el config.xml (en el caso de las librerías).
La filosofía en la que se apoya la extensibilidad de Andami es la siguiente: existen unas clases especiales, llamadas extensiones, que implementan la interfaz IExtension, la cual incluye un método execute() (entre otros). En el fichero config.xml del plugin, se definen una serie de elementos de interfaz de usuario que se van a añadir a la aplicación (herramientas, menús y controles de la barra de estado), y se asocia cada uno de estos elementos con una extensión. En concreto, cuando el usuario pinche en una herramienta o una entrada de menú, Andami ejecutará el método execute de la extensión asociada. De esta forma, la extensión realizará las acciones necesarias para ejecutar la operación solicitada, posiblemente apoyándose en otras clases o librerías del plugin.
Vemos por tanto que la extensión es el punto de acceso a las nuevas funcionalidades aportadas por el plugin, y que los elementos de interfaz de usuario (definidos en el config.xml) junto con la extensión asociada constituyen el nexo entre el resto de la aplicación y el plugin.
<?xml version="1.0" encoding="ISO-8859-1"?>
<plugin-config>
<depends plugin-name="com.iver.cit.gvsig" />
<libraries library-dir="./lib"/>
<resourceBundle name="text"/>
<extensions>
<extension class-name="com.iver.cit.gvsig.wfs.WFSClientExtension"
description="Support to access WFS"
active="true"
priority="1">
</extension>
<extension
class-name="com.iver.gvsig.centerviewpoint.CenterViewToPointExtension"
description="Haces zoom a partir de un par de coordenadas"
active="true">
<menu text="Vista/Centrar_la_Vista_sobre_un_punto"
tooltip="Centrar_la_Vista_sobre_un_punto"
action-command="CENTERVIEWTOPOINT"
icon="images/centerviewtopoint.png" />
<tool-bar name="com.iver.cit.gvsig.Herramientas">
<action-tool icon="images/centerviewtopoint.png"
action-command="CENTERVIEWTOPOINT"
tooltip="Centrar_la_Vista_sobre_un_punto"
enable-text="debería de estar activada" last="true"/>
</tool-bar>
</extension>
</extensions>
</plugin-config>
El fichero utiliza una sintaxis XML, que básicamente implica que tiene una estructura marcada por etiquetas que deben abrirse y posteriormente cerrarse en orden inverso al orden de apertura. Se puede encontrar más información sobre XML en el sitio web del W3 Consortium.
A continuación, se describe en detalle cada una de las etiquetas permitidas. No obstante, dentro de la distribución del código fuente de gvSIG, existe un fichero llamado plugin-config.xsd, situado dentro de _fwAndami/schemas, donde se describen formalmente todas las etiquetas permitidas y el tipo de valores que aceptan. Este fichero está escrito en lenguaje XSD Schema, un lenguaje de descripción de sintaxis XML. Ese fichero debe constituir la referencia principal a la hora de conocer las etiquetas permitidas.
Cabecera
El fichero XML debe ir encabezado por una cabecera como la siguiente:
<?xml version="1.0" encoding="UTF-8"?>
Es importante que el primer carácter < sea el primero del fichero (es decir, que no haya ningún carácter en blanco antes de la cabecera), de lo contrario el fichero se leerá incorrectamente.
<plugin-config>
Etiqueta que simplemente marca el inicio y el fin del fichero de configuración; es la raíz del árbol XML.
<resourceBundle>
Indica el nombre base que tendrán los ficheros de traducción. Por ejemplo, si escribimos <resourceBundle name="text"/>, los ficheros de traducciones se llamarán text.properties, text_en.properties, etc.
<depends>
Indica los plugins de los que va a depender nuestro plugins, a nivel de librerías. Esto significa que si deseamos usar algunas clases o librerías que estén en un plugin distinto, debemos declarar una dependencia de ese plugin. Debe haber una entrada <depends> por cada plugin del que dependamos.
Las dependencias determinan también el orden de carga de plugins, de forma que un plugin siempre se carga después de que sus dependencias han sido ya cargadas. Normalmente, todos los plugins dependen del plugin gvSIG (com.iver.cit.gvsig), por lo que este plugin se carga el primero.
<libraries>
Establece el directorio del que se leerán las librerías de nuestro plugin. Normalmente suele usarse <libraries library-dir="."> (para indicar el directorio raíz del plugin), o bien <libraries library-dir="lib"> (para indicar el subdirectorio lib). Dos plugins distintos pueden tener versiones distintas de una misma librería, y ambas funcionarán correctamente sin interferencias, salvo en los siguientes supuestos:
<label-set>
Indica el comienzo de un grupo de etiquetas (label) para la barra de estado. Cada grupo de etiquetas (label-set) está asociado a una clase, y sólo es visible cuando la ventana visible es instancia de dicha clase. Acepta los siguientes atributos:
<label>
Instala una etiqueta (una caja de texto no editable) en la barra de estada. La etiqueta siempre pertenece a un grupo de etiquetas (label-set) y sólo será visible cuando lo sea el label-set. Acepta los siguientes atributos:
<extensions>
Marca el inicio de la lista de extensiones.
<extension>
Declara una extensión de Andami. Principalmente, la declaración incluye el nombre de la clase Java que implementa esta extensión, y una lista de elementos de interfaz de usuario que deberán añadirse a la aplicación y estarán asociados con esta extensión. <extension> acepta los siguientes atributos:
<tool-bar> Crea una barra de herramientas (grupo de herramientas) con el nombre dado. Dentro esta etiqueta se anidarán los botones que deseamos añadir a esta barra de herramientas. Diferentes extensiones pueden añadir botones a la misma barra de herramientas, basta con que se llamen de la misma forma. <tool-bar> acepta los siguientes atributos:
<action-tool>
Una action-tool es una herramienta (un botón) que lanza una acción concreta. El mecanismo exacto es el siguiente: el usuario pulsa el botón, Andami recibe el evento de pulsado, consulta la extensión asociada a este botón y lanza el método execute(String actionCommand) de dicha extensión, pasándole como parámetro el comando (action-command) definido para este botón. action-tool acepta los siguientes atributos:
<selectable-tool>
Una selectable-tool es un botón que puede estar pulsado (herramienta seleccionada) o no pulsado (herramienta no seleccionada), y no ejecuta una acción como la action-tool, sino que indica un cambio de estado (por ejemplo, "Zoom Más seleccionado", "Zoom Menos seleccionado", etc). Cuando una selectable-tool está pulsada, las demás están no-pulsadas. Si pulsamos en otra selectable-tool, la anterior deja de estar pulsada. La selectable-tool seleccionada en cada momento va asociada a la ventana activa: si cambiamos de ventana activa, se carga la herramienta seleccionada asociada a la nueva ventana activa. Si volvemos a la ventana anterior, se vuelve a seleccionar la herramienta asociada a la ventana anterior.
La etiqueta selectable-tool admite los mismos atributos que action-tool, casi todos con la misma semántica, salvo action-command que en este caso indica la herramienta seleccionada. Adicionalmente, selectable-tool permite:
<menu> Crea una entrada de menú. El menú funcionará a todos los efectos como una action-tool, es decir, al pinchar en el menú se lanzará el método execute() de la extensión asociada, pasándole como parámetro el action-command especificado por el menú. Admite los siguientes atributos:
<combo-scale> Añade a la barra de estado un control de tipo combo box (control que permite elegir un valor de una lista de valores, o bien escribir el valor deseado), con una etiqueta de texto a la izquierda describiendo su función. Acepta los siguientes atributos:
Como ya hemos explicado, las extensiones son clases que sirven de puente entre la funcionalidad básica de gvSIG y las funcionalidades aportadas por nuestro plugin. La extensión recibirá notificación de las acciones realizadas por el usuario, y ejecutará el código apropiado en respuesta a esas acciones (posiblemente usando otras clases de nuestro plugin).
El siguiente diagrama muestra de forma esquemática todas las clases que vamos a explicar en este capítulo y sus relaciones:
Todas las extensiones implementan la interfaz IExtension, que incluye los siguientes métodos:
Los dos siguientes métodos determinan si la extensión (y sus elementos de interfaz de usuario asociados) serán visibles y estarán activos. Estas propiedades se comprueban para todas las extensiones cada vez que se cambia de ventana activa y también justo después de procesar un evento de interfaz de usuario (es decir, después de llamar al método execute de una extensión).
Los dos métodos siguientes sólo se usan con el mecanismo ExclusiveUIExtension, que permite a una extensión tomar el control de la interfaz y activar o desactivar (mostrar u ocultar) otras extensiones. Esto puede usarse para crear extensiones que personalicen gvSIG (por ejemplo, una extensión que transforme gvSIG en un simple visor de cartografía):
Salvo que tengamos una necesidad muy especial, normalmente no implementaremos directamente la interfaz IExtension, sino que extenderemos la clase abstracta Extension, que a su vez ya implementa IExtension. Esto nos ofrece una cierta tranquilidad de futuro en el funcionamiento de nuestra extensión, ya que si se añadiese algún método a la interfaz IExtension, este método se implementaría en la clase Extension de forma que nuestra extensión seguiría funcionando sin cambios (a no ser que necesitase un comportamiento para el nuevo método distinto del implementado en Extension).
La interfaz HiddableExtension y la clase ExtensionDecorator permiten cambiar la visibilidad de una extensión (y sus controles asociados) en tiempo de ejecución, ignorando el valor que indique la propia extensión en el método isVisible. De esta forma podemos instalar una extensión que oculte un conjunto de otras extensiones, creando un gvSIG personalizado (que sólo mostraría las extensiones deseadas).
La interfaz HiddableExtension sólo contiene dos métodos:
La clase ExtensionDecorator implementa la interfaz HiddableExtension. Cada extensión cargada por Andami posee un ExtensionDecorator asociado, que podemos obtener para cambiar la visibilidad de esa extensión. Mostramos como ejemplo el código necesario para ocultar la extensión "com.iver.cit.gvsig.StartEditing", que es la que permite poner una capa en edición. De esta forma, estaríamos desactivando las capacidades de edición de gvSIG:
ExtensionDecorator decorator = PluginServices.getDecoratedExtension(com.iver.cit.gvsig.StartEditing.class);
decorator.setVisibility(ExtensionDecorator.ALWAYS_INVISIBLE);
Por supuesto, para llevar a cabo con éxito una personalización de gvSIG, necesitamos conocer las extensiones que deseamos ocultar. Además, para poder acceder correctamente a la clase de la extensión a desactivar (en este ejemplo, com.iver.cit.gvsig.StartEditing.class) necesitamos declarar una dependencia del plugin que contenga la extensión, de lo contrario obtendremos una excepción de tipo ClassNotFoundException al ejecutar ese código. En este caso, com.iver.cit.gvsig.StartEditing.class está contenido en el plugin com.iver.cit.gvsig.cad, por tanto necesitamos añadir al config.xml de nuestro plugin una línea como la siguiente:
<depends plugin-name="com.iver.cit.gvsig.cad" />
Téngase en cuenta que cualquier extensión puede usar este mecanismo en cualquier momento para ocultar o mostrar otra extensión. Para evitar comportamientos impredecibles, ExtensionDecorator sólo debería usarse en extensiones especiales que deseen personalizar gvSIG, y no deberían instalarse dos extensiones de este tipo al mismo tiempo (de lo contrario, es posible que lo que una de ellas vaya ocultando, la otra lo vaya mostrando y será difícil prever el resultado final, salvo que ambas extensiones realicen estas ocultaciones de forma coordinada).
El mecanismo ExclusiveUIExtension permite realizar algo similar a lo que se consigue con ExtensionDecorator, pero en este caso existe una única extensión que decidirá el estado de las demás. Es decir, con ExtensionDecorator cualquier extensión puede modificar el estado de las demás. Con ExclusiveUIExtension, se definirá una única extensión que tendrá la capacidad de mostrar/ocultar y activar/desactivar el resto de extensiones.
En cualquier caso, ambos mecanismos pueden usarse al mismo tiempo y ExclusiveUIExtension respetará lo definido por ExtensionDecorator, de forma que si ExtensionDecorator define una visibilidad ALWAYS_VISIBLE (siempre visible), la extensión estará visible pese a que ExclusiveUIExtension defina la extensión como oculta. Es decir, ExclusiveUIExtension sólo actuará en caso de que el ExtensionDecorator esté inactivo (modo ExtensionDecorator.INACTIVE). En cualquier caso, ExtensionDecorator sólo actúa sobre la visibilidad de la extensión, mientras que el mecanismo ExclusiveUIExtension puede definir también si la extensión está activa (enabled) o no.
Para usar ExclusiveUIExtension necesitamos: