Download cursoprotect T1 extunderscore desarrollo
Document related concepts
no text concepts found
Transcript
curso_desarrollo Documentation Release 1 Fernando González October 06, 2016 Contents 1 Publicación de versiones 1 2 Prerrequisitos 2 3 JQuery 2 4 OpenLayers 2 5 RequireJS 2 6 Arquitectura 6.1 Estructura del repositorio . . . . . 6.2 error-management . . . . . . . . . 6.3 context-attributes . . . . . . . . . 6.4 request-attributes . . . . . . . . . 6.5 RequireJS . . . . . . . . . . . . . 6.6 Patrón de diseño message-bus 6.7 Arquitectura servidor . . . . . . . . . . . . . . 2 3 9 9 9 9 13 15 7 Configuración de los plugins 7.1 Modificación de la configuración en tiempo de ejecución . . . . . . . . . . . . . . . . . . . . . . . . 7.2 Modificación de la configuración por programación . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 16 17 8 Módulos importantes 8.1 Message-bus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 9 Referencia mensajes 9.1 modules-loaded . . . 9.2 before-adding-layers 9.3 layers-loaded . . . . 9.4 reset-layers . . . . . 9.5 ajax . . . . . . . . . 9.6 error . . . . . . . . . 9.7 info-features . . . . . 9.8 zoom-in . . . . . . . 9.9 zoom-out . . . . . . 9.10 zoom-to . . . . . . . 9.11 initial-zoom . . . . . 18 18 18 18 18 19 19 19 20 20 20 21 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.12 9.13 9.14 9.15 9.16 9.17 9.18 9.19 9.20 9.21 9.22 9.23 9.24 9.25 9.26 9.27 9.28 9.29 9.30 9.31 9.32 9.33 set-default-exclusive-control . . activate-default-exclusive-control activate-exclusive-control . . . . highlight-feature . . . . . . . . clear-highlighted-features . . . . add-group . . . . . . . . . . . . add-layer . . . . . . . . . . . . layer-visibility . . . . . . . . . . time-slider.selection . . . . . . . layer-time-slider.selection . . . . layer-timestamp-selected . . . . toggle-legend . . . . . . . . . . register-layer-action . . . . . . . register-group-action . . . . . . show-layer-panel . . . . . . . . show-info . . . . . . . . . . . . show-layer-info . . . . . . . . . show-group-info . . . . . . . . . show-wait-mask . . . . . . . . . hide-wait-mask . . . . . . . . . activate-feedback . . . . . . . . deactivate-feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 21 22 22 22 22 23 23 24 24 24 24 25 25 25 26 26 26 26 27 27 10 Temas avanzados 10.1 Secuencia de inicio de la aplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 11 Compilación del proyecto 11.1 Obtención de los fuentes . . . . . . . . . . . . 11.2 Instalación de los fuentes en eclipse . . . . . . 11.3 Ejecución del portal en tomcat desde Eclipse . . 11.4 Generación del unredd-portal.war . . . . . . . 11.5 Generación del unredd-portal.war desde eclipse 11.6 Particularidades de la construcción del proyecto . . . . . . 27 27 28 29 30 30 30 12 Ejecución de los tests de integración 12.1 Servicio de base de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.2 Configuración del plugin Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.3 Ejecución de los tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 31 32 13 Manuales 13.1 Hello World . . . . . . . . . . . . . . . . . . . . 13.2 Añadir elementos a la interfaz . . . . . . . . . . 13.3 Añadir una botonera . . . . . . . . . . . . . . . 13.4 Manejo de eventos . . . . . . . . . . . . . . . . 13.5 Posición del mapa . . . . . . . . . . . . . . . . . 13.6 Lectura de parámetros URL . . . . . . . . . . . . 13.7 Servicio hola mundo . . . . . . . . . . . . . . . 13.8 Servicio configuración (1 de 2) . . . . . . . . . . 13.9 Servicio configuración (2 de 2) . . . . . . . . . . 13.10Conexión a base de datos . . . . . . . . . . . . . 13.11Cómo crear un nuevo plugin . . . . . . . . . . . 13.12Cómo crear una nueva aplicación . . . . . . . . . 13.13Cómo dar licencia libre a un plugin . . . . . . . . 13.14Publicación de un plugin en un repositorio Maven 32 32 32 33 34 35 36 36 38 40 44 49 53 60 66 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Contents: 1 Publicación de versiones Este proceso se lanzará cada tres meses. 1. Avisar un par de semanas antes de la publicación. Aunque la fecha esté acordada es una buena práctica avisar. 2. Creación de la rama para la versión en el repositorio: git checkout -b release-<VERSION> mvn versions:set mvn versions:commit git commit -am "Bumped version numbers" Donde la versión tiene la forma x.y.z-SNAPSHOT: • x se incrementa si se rompe la API o el formato del directorio de configuración • y se incrementa cuando se incorporan nuevas funcionalidades • x se incrementa para parches menores La rama se sacará de la última revisión “estable” en la rama principal. 3. Añadimos lista de mejoras a un fichero CHANGELOG. Esto se hace repasando commits desde una fecha determinada, poniendo especial cuidado en inventariar los cambios de API si es una versión mayor. 4. Cuando la rama está lista se elimina el SNAPSHOT de los pom: mvn versions:set mvn versions:commit git commit -am "Bumped version numbers" Se genera un .war y se sube al redmine. En la rama principal se deberá actualizar el número de versión también. De la forma habitual: mvn versions:set mvn versions:commit git commit -am "Bumped version numbers" 5. Se anuncia en la lista incluyendo el changelog y un link al nuevo war. 2 Prerrequisitos La documentación tecnica parte de la base de que hay conocimiento sobre: • maquetación web con HTML y CSS • los lenguajes Java y Javascript • desarrollo de Java Servlets • jQuery básico • OpenLayers 3 JQuery JQuery se puede definir como una librería de utilidad que nos ayuda a realizar las siguientes tareas: • Manipulación del modelo de objetos del documento HTML • Communicación AJAX con el servidor • Communicación entre módulos mediante eventos Ejercicios 4 OpenLayers OpenLayers es una librería que permite la visualizacion e interacción con mapas en navegadores web. Ejercicios 5 RequireJS RequireJS es un cargador de módulos asíncrono. En el contexto del portal se utiliza para cargar los distintos módulos que componen el mismo. Ejercicios 6 Arquitectura El portal es una aplicación cliente/servidor, en la que el cliente está programado mediante módulos Javascript que realizan distintas operaciones. Para realizar estas operaciones, el cliente accede a distintos servicios realizando llamadas asíncronas a servicios que devuelven documentos XML, JSON, etc. con la información necesaria. El lado del servidor se basa en la especificación Servlets 3 de Java. A través de dicha especificación se pueden implementar los servicios que devuelven los documentos XML, JSON, etc. que permitirán a los módulos Javascript desarrollar su funcionalidad. Warning: Aunque la parte servidora se implementa en Java, desde el punto de vista técnico no es estrictamente necesario utilizar Java para implementar los servicios que nos interesen. Los módulos Javascript consumen los servicios a través del protocolo HTTP, que es transparente a los detalles de implementación de los servicios consumidos. En efecto, es posible desplegar una aplicación con la parte cliente accediendo a un conjunto de servicios en PHP, por ejemplo. Sin embargo, aunque la parte cliente acceda a servicios implementados en otro idioma, el sistema de plugins está implementado en Java, como veremos a continuación. Por lo que Java sigue siendo un requisito en el sistema donde se despliegue la aplicación. A continuación se presenta el caso particular del portal (Estructura del repositorio). Se termina presentando la tecnología y patrones usados en el cliente (RequireJS, Patrón de diseño message-bus) así como la especificación Servlet 3 de Java usada en el cliente (Arquitectura servidor). 6.1 Estructura del repositorio Cliente ligero El cliente Javascript es una aplicación modular que se comunica mediante llamadas asíncronas con los servicios web. Desde el punto de vista del navegador, la aplicación tiene la siguiente estructura: unredd-portal |- modules/ |- jslib/ |- styles/ |- indicators |- ... \- index.html -> -> -> -> -> -> RequireJS modulos y sus estilos Librerías Javascript usadas por los módulos: OpenLayers, RequireJS, etc. Hojas CSS generales (de JQuery UI, etc.) Devuelve una lista con información de los indicadores de un objeto en una capa Otros servicios Documento HTML de la aplicación Así, si tubiéramos que añadir una nueva funcionalidad en el cliente, tendríamos que meter los módulos en el directorio modules, las hojas de estilos en styles o en modules y las librerías que se utilicen en jslib. Sin embargo, repartir todos estos ficheros en distintos directorios complicaría el proceso de añadir y quitar plugins a la aplicación, por lo que el código fuente está organizado tratando de agrupar todos esos ficheros por funcionalidad en “plugins”. Plugins, cargador de plugins, aplicaciones Así, para la parte cliente agruparemos los HTML, CSS y Javascript necesarios para implementar las funcionalidades que nos interesen en artefactos que llamaremos plugins. Para la aplicación que queremos desarrollar, habrá que utilizar uno o más plugins, que tendrán que ser cargados mediante el cargador de plugins. Por último, tendremos que desplegar una aplicación, diciéndole al cargador de plugins qué plugins queremos incluir en el resultado final Así, una aplicación incluirá uno o más plugins que serán cargados mediante el cargador de plugins. Estructura del código fuente ¿Cómo están estructurados los plugins, el cargador de plugins y las aplicaciones en el código fuente? La aplicación es implementada por varios proyectos Java estructurados de la siguiente manera: portal |- core/ |- base/ |- feedback/ |- layer-time-sliders/ |- time-slider/ |- language-buttons/ |- geoexplorer-reader/ \- demo/ -> -> -> -> -> -> -> -> Librería que contiene el cargador de plugins y algunas funcionalidades bás Plugin que contiene la funcionalidad básica de la aplicación: árbol de cap Plugin que contiene la funcionalidad de feedback Plugin que muestra una barra temporal por cada capa, para cambiar la insta Plugin que muestra la barra temporal que se instala en la barra de herrami Plugin que muestra los botones para cambiar de idioma. Plugin que lee una base de datos de GeoExplorer para añadir las capas al m Aplicación incluye todos los plugins anteriores. El proyecto base contendrá los módulos RequireJS, librerías Javascript y estilos CSS necesarios para tener todas las funcionalidades del portal, mientras que el proyecto demo especificará de alguna manera que quiere incluir base. Estructura por defecto Maven Todos los proyectos del portal utilizan Maven como herramienta de compilación, adaptándose a la estructura por defecto que por convención tienen los proyectos Maven: • src/main/java: Código fuente Java • src/main/resources: Recursos usados por el código Java, componentes de la parte cliente (CSS, módulos y librerías Javascript, etc.) • src/test/java y src/test/resources: Tests automatizados • pom.xml: Descriptor del proyecto Maven, donde se definen las dependencias entre proyectos. Adicionalmente a estos directorios, las aplicaciones como demo incorporan un directorio adicional: • src/main/webapp: Raíz de la aplicación web Todos los recursos que se sitúen aquí se ofrecerán via HTTP en la raíz de la aplicación por lo que es el lugar ideal para los contenidos estáticos y específicos de la aplicación. Además, incluye el directorio WEB-INF específico de aplicaciones Java, con el descriptor de despliegue web.xml Estructura proyectos plugin Los proyectos plugin constan de los siguientes artefactos: Desarrollos parte cliente Si los proyectos de los que estamos hablando son Java ¿cómo se incluyen artefactos del cliente (módulos RequireJS, CSS, etc.)? Las funcionalidades para la parte cliente se encuentran en el directorio nfms dentro de src/main/resources. Estas funcionalidades consisten en módulos RequireJS, hojas de estilo CSS, librerías Javascript, etc. organizados siguiendo esta estructura: nfms |- xxx-conf.json (descriptor del plugin. xxx es el nombre del plugin. E.g.: base-conf.json) |- modules/ (módulos RequireJS) |- jslib/ (librerías Javascript utilizadas) \- styles/ (hojas de estilo CSS) Este directorio se encuentra en src/main/resources porque Maven por defecto incluirá todo lo que haya ahí en el JAR que genere al empaquetar. Por ejemplo, en el proyecto base existe el directorio src/main/resources que contiene nfms/modules/layer-list.js y nfms/jslib/OpenLayers/OpenLayers.unredd.js, entre otros. Cuando Maven genere el JAR, el directorio nfms aparecerá en la raíz de los contenidos del JAR. Descriptor parte cliente Es un fichero compuesto por el nombre del plugin y “-conf.json” que reside en la raíz del directorio nfms y que contiene información descriptiva sobre el plugin, como la configuracion por defecto de los plugins (ver Configuración de los plugins) o las librerías de terceros que utiliza el plugin y sus dependencias. Esta última información es necesaria para que RequireJS cargue las librerías en el orden correcto. El formato del fichero es el siguiente: { "default-conf" : { "<nombre-modulo>" : <configuracion-por-defecto-modulo> ... }, "requirejs": { "paths" : { "<id-libreria>" : "<ruta relativa a 'modules'>", ... }, "shim" : { "<id-libreria>" : [ "<id-dependencia1>", "<id-dependencia2>", ... ], ... }, } } Ejemplo: { "default-conf" : { "banner" : { "hide" : false } }, "requirejs": { "paths" : { "jquery-ui" : "../jslib/jquery-ui-1.10.4.custom", "fancy-box": "../jslib/jquery.fancybox.pack", "openlayers": "../jslib/OpenLayers/OpenLayers.unredd", "mustache": "../jslib/jquery.mustache" }, "shim" : { "fancy-box": [ "jquery" ], "mustache": [ "jquery" ] }, } } Parte servidora El descriptor de la parte servidora es META-INF/web-fragment.xml y se encuentra en src/main/resources. Sigue el estándar Servlet3 de Java y contiene referencia a las clases Java que implementan los servicios en él declarados. La implementación de los servicios estará en src/main/java. Estructura proyectos aplicación Los proyectos aplicación constan de los siguientes artefactos. TODO Cargador de plugins Para desplegar la aplicación se genera un WAR (Web application ARchive) que contendrá los ficheros JAR pertenecientes a los plugins y sus dependencias. Cuando este WAR se despliega y se inicia la aplicación, se analizan todos los JARs existentes dentro del WAR en busca de módulos RequireJS, estilos y librerías externas. • los paquetes modules y styles son escaneados en busca de módulos javascript y estilos: nfms |- xxx-conf.json |- modules/ (escaneado en busca de .js y .css) |- jslib/ \- styles/ (escaneado en busca de .css) De esta manera, cualquier fichero .css existente en cualquier de los dos paquetes será importado al cargar la aplicación. Igualmente, todo fichero .js existente en modules será cargado inicialmente por RequireJS al iniciar la aplicación. • el descriptor del plugin es analizado. Tras este proceso, todos estos recursos encontrados serán accesibles via HTTP. Despliegue Como visto en el punto Cargador de plugins, todos los JARs incluídos en la aplicación son analizados en busca de módulos, librerías, estilos, etc. Así, para componer una aplicación que incluya los plugins que nos interesan basta con especificar en el pom.xml la dependencia al proyecto del plugin. Cuando este proyecto es incluido como dependencia en un proyecto, por ejemplo demo, aparecerá como JAR dentro del WAR y sus contenidos serán analizados y accesibles via HTTP. Optimización Durante el proceso de empaquetado de una aplicación como fichero WAR se realiza un proceso de optimización de las hojas de estilos CSS y el código Javascript. Este proceso consiste en la generación de dos recursos optimizados para estilos CSS y código Javascript en el directorio optimized del espacio web de dicha aplicación, es decir, en src/main/webapp/. Estos dos ficheros contienen respectivamente todos los estilos CSS y todo el código Javascript proporcionado por todos los plugins incluidos en la aplicación. Además el contenido está comprimido para que la descarga desde el navegador sea más ligera. Así, cuando desplegamos el fichero WAR de la aplicación, éste contiene tanto las hojas de estilo y módulos Javascript individuales como los dos ficheros optimizados. Para seleccionar el modo optimizado basta con poner la variable de entorno MINIFIED_JS=true. A continuación podemos observar lo que nos arroja el fichero index.html en cada caso. Primero sin optimizar: <html> <head> <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="icon" type="image/png" href="static/img/favicon.png"> <link <link <link <link <link <link <link <link <link <link rel="stylesheet" rel="stylesheet" rel="stylesheet" rel="stylesheet" rel="stylesheet" rel="stylesheet" rel="stylesheet" rel="stylesheet" rel="stylesheet" rel="stylesheet" href="modules/banner.css"> href="modules/info-dialog.css"> href="modules/layer-list.css"> href="modules/layout.css"> href="modules/legend-button.css"> href="modules/legend-panel.css"> href="modules/scale.css"> href="modules/time-slider.css"> href="modules/toolbar.css"> href="modules/zoom-bar.css"> <link rel="stylesheet" href="styles/jquery-ui-1.10.3.custom.css"> <link rel="stylesheet" href="styles/jquery.fancybox.css"> <script src="config.js"></script> <!--<script src="js/require.js" data-main="modules/main"></script>--> <script src="jslib/require.js"></script> <script> require.config({ paths: { "main": "modules/main" } }); require(["main"]); </script> <link rel="stylesheet" href="static/overrides.css"/> </head> <body> </body> </html> Y ahora con la variable MINIFIED_JS = true: <html> <head> <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="icon" type="image/png" href="static/img/favicon.png"> <link rel="stylesheet" href="optimized/portal-style.css"> <script src="config.js"></script> <!--<script src="js/require.js" data-main="modules/main"></script>--> <script src="jslib/require.js"></script> <script> require.config({ paths: { "main": "optimized/portal" } }); require(["main"]); </script> <link rel="stylesheet" href="static/overrides.css"/> </head> <body> </body> </html> Podemos observar cómo en lugar de cargarse todos los CSS de forma separada, se carga un único CSS en optimized/portal y que el modulo main se mapea a optimized/portal.js Programación de servicios El código en los módulos RequireJS puede realizar peticiones a los servicios de la aplicación. De igual modo que en la parte cliente, un plugin puede contribuir con servicios a la aplicación final. La implementación de estos servicios se basa en la especificación Java Servlet 3.0 y consistirá en la implementación de uno o más Servlets definidos en el descriptor de despliegue. Este puede encontrarse en dos ficheros. El primero es WEB-INF/web.xml del espacio web, es decir en src/main/webapp/WEB-INF/web.xml en la estructura por defecto de Maven. Este fichero es el descriptor de despliegue propiamente dicho, y en él se pueden definir todos los servlets necesarios en las aplicaciones, como demo. Sin embargo, en los plugins no es posible utilizar el descriptor de despliegue (web-xml) ya que no se genera ningún fichero WAR sino un JAR (que se incluirá en un WAR). En este caso, la especificación Servlet 3.0 define que las librerías JAR usadas por una aplicación WAR pueden contribuir al descriptor de despliegue mediante un fichero META-INF/web-fragment. Es el caso por ejemplo del plugin base que incluye distintos servicios para acceder a indicadores sobre objetos de algunas capas del mapa: <?xml version="1.0" encoding="UTF-8"?> <web-fragment version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-f <!-- indicators --> <servlet> <servlet-name>indicator-list-servlet</servlet-name> <servlet-class>org.fao.unredd.indicators.IndicatorListServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>indicator-list-servlet</servlet-name> <url-pattern>/indicators</url-pattern> </servlet-mapping> <servlet> <servlet-name>indicator-data-servlet</servlet-name> <servlet-class>org.fao.unredd.indicators.IndicatorDataServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>indicator-data-servlet</servlet-name> <url-pattern>/indicator</url-pattern> </servlet-mapping> </web-fragment> 6.2 error-management 6.3 context-attributes 6.4 request-attributes 6.5 RequireJS RequireJS es una librería que permite encapsular en módulos nuestro código JavaScript y cargarlos bajo demanda (llamada require). Para ello es necesario cargar la librería RequireJS en el código HTML de la página en la que se va a utilizar: <html> <head> <script src="require.js" type="text/javascript" data-main="modules/main"></script> </head> <body> </body> </html> En el tag de carga se especifica el módulo inicial que RequireJS cargará y ejecutará. En el ejemplo “modules/main.js”. Módulos RequireJS Un módulo RequireJS consiste en: • Unas dependencias • Un código de inicialización que devuelve opcionalmente un valor y que se ejecuta cuando las dependencias han sido a su vez inicializadas Árbol de dependencias Típicamente los módulos consisten en una llamada a define, que toma una lista de los módulos y una función de inicialización que se ejecuta cuando las dependencias están satisfechas: define([ "map" ], function(map) { alert("este módulo depende del módulo" + "map y en este punto map ha sido" + "cargado e inicializado"); }); La función obtiene recibe como parámetros referencias a los módulos que se definen como dependencias, en el mismo orden: define([ "layer-list", "map" ], function(layerList, map) { // layerList apunta al módulo "layer-list" // map apunta al módulo "map" }); pero el orden de carga puede variar, ya que por ejemplo, en el caso anterior otro módulo puede haber cargado ya layer-list. RequireJS analiza las llamadas define y crea un árbol de dependencias que se cargan en cualquier orden que garantice que las dependencias de un módulo estén todas cargadas e inicializadas cuando el módulo que depende de ellas se inicializa. De forma más visual: para cargar A requireJS realiza una secuencia similar a esta: • Se detecta la dependencia de A en B y C por lo que pasa a intentar cargarlos. • Se intentar cargar B pero B depende de E, por lo que se intenta cargar E. • Como E no tiene ninguna dependencia se cargar directamente y se llama a su código de inicialización. • Ahora que B tiene todas sus dependencias satisfechas se ejecuta su código de incialización. • RequireJS pasa a cargar C, pero detecta la dependencia en D y en E y, como E ya ha sido cargada, pasa a cargar D. • Como D no tiene ninguna dependencia se cargar directamente y se llama a su código de inicialización. • Ahora C tiene sus dos dependencias ya cargadas, por lo que se llama a su código de incialización. • Con B y C ya cargados se pasa a ejecutar el código de inicialización de A. En la secuencia anterior se ilustra por una parte el hecho de que los módulos en los que depende un módulo son cargados e inicializados previamente. Por otra parte, se muestra que el orden de carga de las dependencias de un módulo no corresponde con el orden en el que están definidas en el código. Por ejemplo, en el caso de C primero se cargó E antes que D porque la dependencia fue cargada por B previamente. Dependencias de módulos no-RequireJS En el caso de un módulo que dependa de librerías que no son módulos RequireJS (jquery, openlayers, etc.) es todavía necesario especificar las dependencias, ya que de lo contrario RequireJS puede cargarlas arbitrariamente antes o después. En caso de hacerlo después, el módulo intentará utilizar una librería que no está cargada y se producirá un error. La forma de especificar la dependencia un modulo tal es idéntica a la de los modulos RequireJS: define([ "jquery" ], function($) { alert("este módulo depende de jQuery" + "y en este punto jQuery ha sido" + "cargado e inicializado"); }); pero ¿de dónde saca RequireJS el código de jQuery? No lo puede obtener de ningún sitio, por lo que hay que especificárselo. El módulo main, que es el módulo de entrada, es el encargado de realizar esta configuración: require.config({ baseUrl : "modules", paths : { "jquery" : "http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min", "jquery-ui" : "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min", "fancy-box" : "../js/jquery.fancybox.pack", "openlayers" : "../js/OpenLayers/OpenLayers.debug" }, shim : { "fancy-box" : [ "jquery" ] } }); En el código de main se puede observar cómo hay una llamada al método require.config que incluye un elemento cuya propiedad paths incluye las direcciones de las cuales puede descargar RequireJS cada librería. También se puede observar cómo en la propiedad shim se establecen las dependencias entre módulos no-RequireJS. Valores de retorno Se ha mostrado anteriormente cómo las dependencias de un módulo son recibidas como parámetro en la función de inicialización: define([ "layer-list", "map" ], function(layerList, map) { // layerList apunta al módulo "layer-list" // map apunta al módulo "map" }); pero, ¿qué contienen exactamente estas referencias? Contienen el valor que retorna la función de inicialización del módulo dependencia. Por ejemplo, el siguiente módulo map en su función de inicialización crea un mapa OpenLayers, añade una capa y la devuelve: define([ "openlayers" ], function() { var mimapa = new OpenLayers.Map("map"); mimapa.addLayer(new OpenLayers.Layer.WMS("OpenLayers WMS", "http://vmap0.tiles.osgeo.org/wms/ layers : "basic" })); return mimapa; }); Por tanto, cuando el siguiente módulo establezca la dependencia: define([ "map" ], function(map) { map.setCenter(new OpenLayers.LonLat(-84, 0), 6); }); el valor de la variable map que se pasará a la función de inicialización de éste será el mapa OpenLayers que se creó en el módulo map anterior. Funciones públicas A veces un módulo debe permitir que los módulos que dependen de él realicen algunas operaciones sobre el mismo. Más adelante se verá que existe otra manera que frecuentemente es mejor, pero de momento mostramos otra manera que también puede ser útil en un momento dado. Por ejemplo, si queremos hacer un módulo error para manejar los errores de la aplicación mostrando un diálogo al usuario y escribiendo el problema en la consola tendríamos una función como ésta: function(errorMsg) { console.log(errorMsg); alert(errorMsg); }); Para poder utilizar este módulo tendremos que importarlo en las dependencias de nuestro módulo y cuando se produzca la condición del error hacer una llamada a dicho módulo: define([ "error" ], function(error) { if (condicion) { error.showMessage("Se ha cumplido una condición de error"); } }); Pero para esto tendríamos que devolver un objeto en el módulo error que tenga una función showMessage. Esto podría hacerse teniendo un módulo error como el siguiente: define([], function() { return { showMessage : function(errorMsg) { console.log(errorMsg); alert(errorMsg); } }; }); cuyo valor de retorno tiene una propiedad showMessage que es la función de error. Funciones privadas Es posible definir funciones privadas dentro del módulo. Para ello basta con definir las funciones antes del valor de retorno del módulo: define([ "openlayers" ], function() { function createLayer(name, url, wmsName) { return new OpenLayers.Layer.WMS(name, url, { layers : wmsName, transparent : true }); } var mimapa = new OpenLayers.Map("map"); mimapa.addLayer(createLayer("Basic", "http://vmap0.tiles.osgeo.org/wms/vmap0?", "basic"); mimapa.addLayer(createLayer("Costa", "http://vmap0.tiles.osgeo.org/wms/vmap0?", "coastline_01 return mimapa; }); En el ejemplo anterior se define una función createLayer y a continuación se le invoca un par de veces para instanciar las capas WMS que se añaden al mapa. Plantilla módulo A continuación se presenta una plantilla que puede ser útil para la creación de nuevos módulos: define([ "dependencia1", "dependencia2" ], function(modulo1, modulo2) { // // variables privadas // var miVariablePrivada = ...; // // funciones privadas // function miFuncionPrivada() { // } // // inicialización // // valor de retorno // return { // propiedades públicas miPropiedadPublica : ..., // Funciones públicas miFuncionPublica : function() { } }; }); 6.6 Patrón de diseño message-bus El patrón de diseño Message Bus permite desacoplar los componentes que forman una aplicación. En una aplicación modular, los distintos componentes necesitan interactuar entre sí. Si el acoplamiento es directo, la aplicación deja de ser modular ya que aparecen dependencias, con frecuencia recíprocas, entre los distintos módulos y no es posible realizar cambios a un módulo sin que otros se vean afectados. En cambio, si los objetos se acoplan a través de un objeto intermediario (Message Bus), casi todas las dependencias desaparecen, dejando sólo aquellas que hay entre el Message Bus y los distintos módulos. En el siguiente ejemplo vemos una hipotética aplicación modular que consta de tres componentes con representación gráfica y que están dispuestos de la siguiente manera: • Map: En la parte central (verde) hay un mapa que muestra cartografía de España. • LayerList: En la parte izquierda (rojo) hay una lista de temas. Vemos que sólo hay un tema de catastro, que es el que se visualiza en el mapa. • NewLayer: En la parte superior (azul) existe un control que permite añadir temas a los otros dos componentes. Un posible diseño de dicha página consistiría en un módulo Layout que maqueta la página HTML y que inicializa los otros tres objetos. En respuesta a la acción del usuario, el objeto NewLayer mandaría un mensaje a LayerList y Map para añadir el tema en ambos componentes. De la misma manera, LayerList podría mandar un mensaje a Map en caso de que se permitiera la eliminación de capas desde aquél. El siguiente grafo muestra los mensajes que se pasarían los distintos objetos: Es posible observar como en el caso de que se quisiera quitar el módulo LayerList, sería necesario modificar el objeto Layout así como el objeto NewLayer, ya que están directamente acoplados. Sin embargo, con el uso del Message Bus, sería posible hacer que los distintos objetos no se referenciaran entre sí directamente sino a través del Message Bus: Así, el módulo NewLayer mandaría un mensaje al Message Bus con los datos de la nueva capa y Map y LayerList símplemente escucharían el mensaje y reaccionarían convenientemente. Sería trivial quitar de la página LayerList ya que no hay ninguna referencia directa al mismo (salvo tal vez en Layout). Y al contrario: sería posible incluir un nuevo módulo, por ejemplo un mapa adicional, y que ambos escuchasen el evento “add-layer” de forma que se añadirían los temas a ambos mapas. De esta manera la aplicación es totalmente modular: es posible reemplazar módulos sin que los otros módulos se vean afectados, se pueden realizar contribuciones bien definidas que sólo deben entender los mensajes existentes para poder integrarse en la aplicación, etc. TODO referenciar los ejemplos del taller de desarrollo. 6.7 Arquitectura servidor Introducción a servlet 3.0 La parte servidor del portal está construida sobre la especificación Servlet 3.0. En este contexto se pueden crear principalmente tres tipos de objetos: filtros, servlets y escuchadores de contexto o ContextListeners. Servlets Los servlets son classes que heredan de javax.servlet.http.HttpServlet: package org.fao.unredd; import javax.servlet.http.HttpServlet; public class HolaMundoServlet extends HttpServlet { } El objetivo principal del servlet es proporcionar una respuesta a una petición HTTP. Si por ejemplo queremos retornar una petición GET deberemos de implementar el método doGet: package org.fao.unredd; import java.io.IOException; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; public class HolaMundoServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String nombre = req.getParameter("nombre"); resp.getWriter().println("Hola mundo, " + nombre); } } Podemos observar que es posible utilizar la instancia de HttpServletRequest que se pasa en el parámetro req para acceder a los parámetros de la petición GET, en este caso nombre; y que podemos escribir la respuesta a través de la instancia de HttpServletResponse. Por último se necesario especificar al sistema en qué URL se debe acceder a dicho servlet. Para ello hay que registrarlo en el fichero web.xml de la siguiente manera: <servlet> <servlet-name>holamundo-servlet</servlet-name> <servlet-class>org.fao.unredd.HolaMundoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>holamundo-servlet</servlet-name> <url-pattern>/holamundo</url-pattern> </servlet-mapping> En el elemento servlet se le dice al sistema que hay un servlet de nombre holamundo-servlet y que es implementado por la clase org.fao.unredd.HolaMundoServlet (vista más arriba), mientras que en el segundo elemento se establece la URL en la que se puede acceder al servlet. Con esta configuración sería posible acceder al servlet en una URL similar a esta: http://localhost:8080/app/holamundo Note: La especificación Servlet 3.0 permite la inclusión de anotaciones en la clase que implementa el servlet para especificar la misma información que contiene el web.xml, de tal manera que el fichero no es necesario. Sin embargo, por simplicidad se evita el uso de anotaciones de este tipo en general. Filtros 7 Configuración de los plugins Con anterioridad se ha comentado que el descriptor del plugin “xxx-conf.json” incluye un elemento para la configuración de los distintos modulos RequireJS que forman el plugin. La configuración que se especifica en dichos elementos queda accesible a los módulos RequireJS mediante el método config() meta-modulo module. Por ejemplo, si tuviéramos el siguiente descriptor de plugin: { "default-conf" : { "mi-modulo" : { "mensaje" : "hola mundo" } } } el siguiente módulo, definido en “mi-modulo.js” podría acceder a su configuración así: define([ "module" ], function(module) { alert(module.config()); }); mostrando por pantalla el valor de su configuración, es decir el mensaje “hola mundo”. 7.1 Modificación de la configuración en tiempo de ejecución Ahora bien, esta configuración está definida en el plugin de forma fija y sólo se puede cambiar tocando el códido del portal y generando un nuevo WAR, lo cual no es nada práctico. ¿Cómo se puede cambiar la configuración de un plugin de la aplicación una vez ésta está desplegada y ejecutándose en el servidor? La manera más sencilla consiste en modificar el fichero plugin-conf.json que se encuentra en el directorio de configuración del portal. Este fichero tiene la misma estructura que el descriptor del plugin con la única diferencia de que es usado sólo para sobreescribir la configuración por defecto de los distintos módulos. Así, podríamos editar el fichero para dejarlo de esta manera: { "default-conf" : { "mi-modulo" : { "ejemplo" : "hola a todo el mundo" } } } Y al cargar el módulo mi-modulo aparecerá por la pantalla “hola a todo el mundo”, en lugar de “hola mundo”. 7.2 Modificación de la configuración por programación En ocasiones la configuración que se quiere pasar al plugin depende de un valor de la base de datos, o de si el usuario está logado o, en general, de aspectos que se tienen que comprobar por programación. ¿De qué manera es posible hacer llegar estos valores a un elemento de la interfaz de usuario? La solución son los proveedores de configuración. Los proveedores de configuración son instancias que implementan la interfaz org.fao.unredd.portal.ModuleConfigurationProvider, que permite añadir elementos a la configuración de los módulos de la misma manera que se haría manualmente modificando el fichero plugin-conf.json. Para que la instancia contribuya a la configuración hay que registrarla en la instancia config, por lo que normalmente se registrará en un context listener con un código similar al siguiente: ServletContext servletContext = sce.getServletContext(); Config config = (Config) servletContext.getAttribute("config"); config.addModuleConfigurationProvider(new MiConfigurationProvider()); 8 Módulos importantes Entre los módulos más importantes existentes en la plataforma podemos destacar: • message-bus: Implementación del patrón message bus (Ver Patrón de diseño message-bus) • communication: Facilita la comunicación con el servidor • error: Gestión centralizada de los errores • layout: Crea el layout de la página • i18n: Contiene las cadenas de los ficheros de traducción .properties • map: Crea el mapa principal • layers: Lee la configuración de capas y lanza eventos add-layer y add-group • customization: Carga el resto de módulos 8.1 Message-bus De los módulos anteriores, el más importante es message-bus Función principal: Ofrecer dos métodos para mandar y escuchar mensajes al/del bus. La documentación de las dos funciones puede encontrarse en el código fuente: https://github.com/nfms4redd/nfms/blob/develop/portal/src/main/webapp/modules/message-bus.js Valor de retorno: Un objeto con dos propiedades send y listen que permiten respectivamente enviar y escuchar mensajes. Mensajes enviados: Ninguno. El módulo se encarga de procesar y canalizar los eventos, pero no inicia ni escucha ninguno. Mensajes escuchados: Ninguno. 9 Referencia mensajes 9.1 modules-loaded Enviado una vez el módulo customization ha cargado todos los módulos especificados por el servidor. Parámetros: Ninguno. Ejemplo de uso: Útil para realizar acciones que requieren que todos los módulos hayan sido cargados. Por ejemplo, el envío de mensajes de potencial interés para algún módulo ha de hacerse tras la carga de todos los módulos, es decir, una vez este mensaje ha sido enviado. Más información: • Secuencia de inicio de la aplicación 9.2 before-adding-layers Enviado justo antes de que se empiecen a lanzar los eventos add-group y add-layer. Da la oportunidad a otros módulos de realizar operaciones previas a la carga de las capas. Parámetros: Ninguno. Más información: • Secuencia de inicio de la aplicación 9.3 layers-loaded Enviado una vez el módulo layers ha lanzado los eventos add-layer y add-group correspondientes a la configuración de capas existente en el servidor. Parámetros: Ninguno Ejemplo de uso: Útil para realizar acciones que requieren que las capas de información se hayan cargado, por ejemplo, para centrar el mapa. Más información: • Secuencia de inicio de la aplicación • moduleconfiguration 9.4 reset-layers Se envía para resetear la configuración de capas del portal. Como norma general, cualquier módulo que escuche los eventos add-layer, add-group o layers-loaded también deberá escuchar el evento reset-layers, y devolver el estado interno del módulo (y del DOM) al que tenía justo antes de empezar a cargar grupos y capas. Parámetros: Ninguno Más información: • add-layer • add-group • layers-loaded 9.5 ajax Escuchado por el módulo communication para realizar llamadas a servicios. Parámetros: Un objeto con las siguientes propiedades: • url: URL a la que se quiere pedir la información • success: función a ejecutar cuando el servidor responda satisfactoriamente • complete: función a ejecutar cuando el servidor responda, sea satisfactoriamente o tras un error • errorMsg: Mensaje de error • error: función a ejecutar cuando el servidor responda con un error. Por defecto se generará un mensaje de error con el contenido de errorMsg. • controlCallBack: función que recibe el objeto XMLHttpRequest que representa la petición. Este objeto tiene métodos como abort() que permiten la cancelación de la petición Ejemplo de uso: 9.6 error Escuchado por el módulo error, que muestra un mensaje de error al usuario: Parametros: Mensaje con el error a mostrar Ejemplo de uso: bus.send("error", "Dirección de e-mail incorrecta"); 9.7 info-features Resultados de la petición de información a una única capa. Parámetros: • wmsLayerId: Id de la capa a la que pertenecen las features. • features: array con las features OpenLayers. Cada feature tiene: – Una propiedad “aliases” que es un array que contiene un objeto con propiedades “name” y “alias” para cada atributo de la feature. Por ejemplo: [{ "name" : "ident", "alias" : "Id" }, { "name" : "nprov", "alias" : "Nombre" }, { "name" : "pop96", "alias" : "Población (1996)" }] – Una propiedad “bounds” con el bounding box de la geometría de la feature o null si el servidor no la devolvió. Siempre en EPSG:900913. – Una propiedad highlightGeom con la geometría de la feature o el bounding box (en caso de que así se configure en el layers.json) o null si el servidor no devolvió datos geométricos. Siempre en EPSG:900913. • x: Posición X en la que se hizo click para obtener la información • y: Posición Y en la que se hizo click para obtener la información Ejemplo de uso: Más información: 9.8 zoom-in Mueve la escala al nivel inmediatamente mayor Parámetros: Ninguno Ejemplo de uso: bus.send("zoom-in"); Más información: 9.9 zoom-out Mueve la escala al nivel inmediatamente menor Parámetros: Ninguno Ejemplo de uso: bus.send("zoom-out"); Más información: 9.10 zoom-to Mueve el encuadre al objeto OpenLayers.Bounds que se pasa como parámetro. El objeto bounds debe estar en el sistema de referencia del mapa (EPSG:900913) Parámetros: OpenLayers.Bounds con el extent deseado Ejemplo de uso: var bounds = new OpenLayers.Bounds(); bounds.extend(new OpenLayers.LonLat(0,42)); bounds.extend(new OpenLayers.LonLat(10,52)); bounds.transform( new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913")); bus.send("zoom-to", bounds); Más información: 9.11 initial-zoom Situa el mapa en la posición inicial Parámetros: Ninguno Ejemplo de uso: bus.send("initial-zoom"); Más información: 9.12 set-default-exclusive-control Establece el control exclusivo por defecto para el mapa. Sólo un módulo exclusivo está activado en cada momento. Parámetros: Objeto OpenLayers.Control. Ejemplo de uso: var control = new OpenLayers.Control.WMSGetFeatureInfo({ ... }); bus.send("set-default-exclusive-control", [control]); Más información: 9.13 activate-default-exclusive-control Activar el control establecido por defecto mediante el mensaje set-default-exclusive-control Parámetros: Ninguno Ejemplo de uso: bus.send("activate-default-exclusive-control"); Más información: 9.14 activate-exclusive-control Pide la activación exclusiva del control que se pasa como parámetro y la desactivación del control exclusivo que estuviera activado en el momento de lanzar el mensaje Parámetros: OpenLayers.Control Ejemplo de uso: var clickControl = new OpenLayers.Control({ ... }); bus.send("activate-exclusive-control", [ clickControl ]); Más información: 9.15 highlight-feature Indica que se debe resaltar la geometría que se pasa como parámetro Parámetros: OpenLayers.Geometry Ejemplo de uso: Más información: 9.16 clear-highlighted-features Indica que se deben de eliminar todos los resaltes establecidos mediante highlight-feature Parámetros: Ninguno. Ejemplo de uso: Más información: 9.17 add-group Indica que se debe añadir un grupo al árbol de capas Parámetros: Un objeto con las siguientes propiedades: • id: identificador del grupo • parentId: Opcional, para grupos dentro de otros grupos hace referencia al grupo contenedor • name: nombre del grupo • infoLink: Ruta de la página HTML con información sobre el grupo Ejemplo de uso: bus.send("add-group", [ { id:"grupo_admin", name:"Límites administrativos" }]); Más información: 9.18 add-layer Indica que se debe añadir una capa a la aplicación Parámetros: Un objeto con las siguientes propiedades: • id: id de la capa • groupId: id del grupo en el que se debe añadir la capa • label: Texto con el nombre de la capa a usar en el portal • infoLink: Ruta de la página HTML con información sobre la capa • inlineLegendUrl: URL con una imagen pequeña que situar al lado del nombre de la capa en el árbol de capas • queryable: Si se pretende ofrecer herramienta de información para la capa o no • active: Si la capa está inicialmente visible o no • wmsLayers: Array con la información de las distintas capas WMS que se accederán desde esta capa. El caso más habitual es que se acceda sólo a una, pero es posible configurar varias. Los objetos de este array tienen la siguiente estructura: – baseUrl: URL del servidor WMS que sirve la capa – wmsName: Nombre de la capa en el servicio WMS – imageFormat: Formato de imagen a utilizar en las llamadas WMS – zIndex: Posición en la pila de dibujado – legend: Nombre del fichero imagen con la leyenda de la capa. static/loc/{lang}/images Estos ficheros se acceden en – label: Título de la leyenda – sourceLink: URL del proveedor de los datos – sourceLabel: Texto con el que presentar el enlace especificado en sourceLink – timestamps: Array con los instantes de tiempo en ISO8601 para los que la capa tiene información Ejemplo de uso: bus.send("add-layer", { "id" : "meteo-eeuu", "groupId" : "landcover", "label" : "Radar EEUU", "active" : "true", "wmsLayers" : [ { "baseUrl" : "http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi", "wmsName" : "nexrad-n0r-wmst" } ] }); Más información: 9.19 layer-visibility Cambia la visibilidad de una capa Parámetros: • id de la capa portal • valor de visibilidad Ejemplo de uso: bus.send("layer-visibility", ["provincias", false]); Más información: 9.20 time-slider.selection Lanzado cuando el usuario selecciona un instante temporal global distinto al actual. Generalmente se actualiza el mapa con la información de esa fecha. Parámetros: objeto Date con el instante temporal seleccionado Ejemplo de uso: var d = new Date(); bus.send("time-slider.selection", d); Más información: 9.21 layer-time-slider.selection Lanzado cuando el usuario selecciona un instante temporal específico para una capa (a diferencia del time-slider.selection cuyo instante es global para todas las capas). Parámetros: • id de la portalLayer que ha determinado su instante temporal. • objeto Date con el instante temporal seleccionado Ejemplo de uso: var d = new Date(); bus.send("layer-time-slider.selection", ["mi-portal-layer", date]); Más información: 9.22 layer-timestamp-selected Una capa ha escuchado uno de los eventos de selección temporal y ha determinado qué instancia temporal es la que más se ajusta a esa. La capa selecciona la última instancia temporal que es menor o igual al instante seleccionado o la primera instancia si el instante seleccionado es anterior a todas sus instancias. Parámetros: • id de la portalLayer que ha determinado su instante temporal. • objeto Date con el instante temporal seleccionado • cadena de carácteres con el nombre del estilo que se debe usar para esta instancia temporal. Puede ser nulo si la capa no requiere un estilo distinto por instante. Más información: 9.23 toggle-legend Escuchado por el módulo legend-panel para mostrar u ocultar el panel con la leyenda. Parámetros: Ninguno Ejemplo de uso: bus.send("toggle-legend"); Más información: 9.24 register-layer-action Escuchado por la lista de capas. Instala un botón a la derecha de las capas que realizará una acción al ser pulsado. Parámetros: Función que devuelve el objeto jQuery que se mostará a modo de botón. Esta función toma como parámetro el mismo objeto que se lanza en el evento add-layer. Ejemplo de uso (botón de información): bus.listen("before-adding-layers", function() { var showInfoAction = function(portalLayer) { if (portalLayer.hasOwnProperty("infoFile")) { aLink = $("<a/>").attr("href", portalLayer.infoFile); aLink.addClass("layer_info_button"); aLink.fancybox({ "closeBtn" : "true", "openEffect" : "elastic", "closeEffect" : "elastic", "type" : "iframe", "overlayOpacity" : 0.5 }); return aLink; } else { return null; } }; bus.send("register-layer-action", showInfoAction); }); Más información: 9.25 register-group-action Igual que register-layer-action pero para grupos. 9.26 show-layer-panel Activa el panel de capas indicado. Parámetros: identificador del panel a activar. La lista de paneles puede variar en función de los plugins que haya activados. La lista completa de ids es: • all_layers_selector • layers_transparency_selector • layer_slider_selector (sólo con el plugin layer-time-sliders) Ejemplo de uso: bus.send("show-layer-panel", [ "layers_transparency_selector" ]); Más información: 9.27 show-info Muestra una ventana emergente con determinada información que se pasa como parámetro. Parámetros: • title: Título de la ventana • link: Bien una url que apunta a la página que se pretende mostrar, o un objeto jquery que será mostrado en la ventana • eventOptions: Opcional. Elemento con las opciones para la personalización de la ventana. Actualmente se utiliza FancyBox por lo que se puede añadir cualquier opción válida de este framework. Ejemplo de uso: bus.send("show-info", [ "Mi info", "http://ambiente.gob.am/portal/static/loc/es/html/doc.html" ]); Más información: 9.28 show-layer-info Muestra la información asociada a una capa con su atributo infoLink o infoFile. Parámetros: identificador de la capa. Ejemplo de uso: bus.send("show-layer-info", [ "provincias" ]); 9.29 show-group-info Muestra la información asociada a un grupo con su atributo infoLink o infoFile. Parámetros: identificador del grupo Ejemplo de uso: bus.send("show-group-info", [ "base" ]); 9.30 show-wait-mask Muestra un indicador de que el sistema está ocupado y el usuario debe esperar Parámetros: Texto informativo Ejemplo de uso: bus.send("show-wait-mask", "Enviando información al servidor..."); 9.31 hide-wait-mask Oculta el indicador mostrado por show-wait-mask Parámetros: Ninguno Ejemplo de uso: bus.send("hide-wait-mask"); 9.32 activate-feedback Activa el modo feedback mostrando la ventana y seleccionando la herramienta para el dibujado del polígono sobre el que se da el feedback. Parámetros: ninguno Ejemplo de uso: bus.send("activate-feedback"); 9.33 deactivate-feedback Desactiva el modo feedback, ocultando la ventana y volviendo a la herramienta por defecto (navegación). Parámetros: ninguno Ejemplo de uso: bus.send("deactivate-feedback"); 10 Temas avanzados 10.1 Secuencia de inicio de la aplicación La aplicación se inicia generando el fichero index.html mediante un motor de plantillas. Dicho motor rellena: • Los tags style con las referencias a los ficheros .css del directorio de módulos. • Una llamada al servicio config.js con el parámetro lang establecido al valor con el que se accedió a index.html (si se accedió con index.html?lang=es se generará la carga de config.js?lang=es). La llamada al servicio config.js devuelve un fichero javascript con la configuración de los distintos módulos. La configuración relevante para el inicio de la aplicación es la del módulo customization, que incluye la lista de módulos existente en el fichero portal.properties en la propiedad client.modules. El módulo customization obtiene la lista de módulos de la configuración y se realiza la siguiente secuencia: 1. customization hace una llamada a require para cargar los módulos. 2. una vez cargados, se lanza el mensaje modules-loaded 3. el evento es escuchado por el módulo layers que fue cargado en el primer paso. layers lanza el evento before-adding-layers y a continuación procesa el árbol de capas y lanza los mensajes add-group y add-layer correspondientes. 4. layers lanza el mensaje layers-loaded 11 Compilación del proyecto 11.1 Obtención de los fuentes El código del portal se encuentra alojado en el siguiente repositorio de GitHub: https://github.com/nfms4redd/portal. GitHub proporciona un servicio de GIT, un conocido sistema de control de versiones. Para aquellos que tengan conocimientos de uso de GIT, la URL del servicio es git@github.com:nfms4redd/portal.git. Bastaría con ejecutar la siguiente instrucción para tener una copia de los fuentes instalada en local: $ git clone git@github.com:nfms4redd/portal.git En el caso de que no se quiera utilizar GIT, es posible descargarse un fichero zip con el estado actual del repositorio pulsando el botón “Download ZIP” que ofrece GitHub en la página del repositorio. Hay que tener en cuenta que la rama por defecto, llamada “develop”, es en la que se realizan los desarrollos y es posible que sea algo más inestable. En el combo “branch:” es posible elegir otras versiones del código fuente, como por ejemplo las ramas que comienzan por “release_” que son más estables. 11.2 Instalación de los fuentes en eclipse Una vez el código fuente ha sido descargado, es necesario abrir Eclipse e importar un proyecto Maven existente mediante un clic con el botón derecho en el Project Explorer > Import > Import... En el diálogo que aparece hay que decirle a Eclipse que el proyecto que queremos importar es un proyecto Maven. Así, habrá que seleccionar Maven > Existing Maven Projects A continuación hay que seleccionar el directorio en el que hemos puesto el portal y darle a siguiente. Tras este paso, nos aparecerá una ventana en la que podremos seleccionar los proyectos dentro del repositorio del portal que queremos importar: Como se ve en la imagen, seleccionaremos todos menos el proyecto raíz y haremos clic en el botón de finalizar. Es posible que Eclipse instale algunos plugins tras esta acción. Warning: El repositorio del portal contiene varios proyectos correspondientes a la aplicación del portal, plugins, herramientas, etc. Para más información sobre cómo están organizados los proyectos dentro del repositorio ver Estructura del repositorio 11.3 Ejecución del portal en tomcat desde Eclipse Para arrancar un Tomcat que contenga el portal, es necesario hacer clic derecho en uno de los proyectos aplicación (ver Estructura del repositorio), como demo, y seleccionar en el menú contextual que aparece: Debug As > Debug on server. En el caso de no tener un servidor instalado aparecerá un diálogo que nos permitirá definir un nuevo servidor. Seleccionaremos “Tomcat v7.0 Server” y en la siguiente pantalla seleccionaremos el directorio donde se encuentra nuestro Tomcat. Obviamente, es necesario haber descargado e instalado previamente un servidor Apache Tomcat 7.0. Tras esta operación podremos aceptar el diálogo, tras lo cual el servidor se ejecutará y se abrirá una ventana dentro de eclipse con el portal. También debe ser posible acceder al mismo mediante la siguiente URL: http://localhost:8080/unredd-portal/ desde cualquier navegador web. 11.4 Generación del unredd-portal.war Si lo que se pretende es exclusivamente obtener el fichero .war, es posible obviar Eclipse y utilizar directamente Maven desde línea de comandos: $ mvn package tras el cual el fichero demo/target/unredd-portal.war habrá aparecido. Dicho fichero se puede desplegar en el directorio webapps de Tomcat para poder usar la aplicación recién compilada. 11.5 Generación del unredd-portal.war desde eclipse Desde eclipse, se podría generar el war clicando con el botón derecho del ratón en el proyecto portal -> Run as -> Maven Build. En la ventana que aparece, escribir “package” en el texto Goals: y seleccionar la casilla “Skip Tests”. Tras pinchar en el botón Run se iniciará la ejecución. Si hacia el final aparece el texto: [INFO] -----------------------------------------------------------------------[INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ todo estará correcto y aparecerá un fichero unredd-portal.war en el directorio target. 11.6 Particularidades de la construcción del proyecto Maven tiene un método estandarizado para la construcción de proyectos en Java. Este método sigue una secuencia de actividades o ciclo de vida (explicado aquí) que define que primero se realiza la compilación, luego un testeo, el empaquetado, etc. y que es común para todos los proyectos Maven. Sin embargo, a la hora de construir el portal, hay algunas particularidades que hay que tener en cuenta. Optimización Por una parte, durante la construcción del proyecto se realizan una optimización de los recursos del cliente (ver Optimización) que puede tomar bastante tiempo. En algunos casos es posible que esta optimización no sea necesaria. Para solucionar esto, Maven proporciona perfiles, que son configuraciones que se pueden activar y desactivar. Así, la optimización está configurada en un perfil “optimize” que está activo por defecto pero se puede desactivar con el parámetro -P, seguida del nombre del perfil con un signo de exclamación delante (para desactivar): mvn -P \!optimize package Tests de integración Por otra parte, una vez se construye el WAR en la fase package de Maven, se llega a la fase integration-test. En el caso del portal, se testean las conexiones a bases de datos, etc. por lo que se requiere levantar determinados servicios externos para que la fase se pase con éxito. Para más información ver Ejecución de los tests de integración 12 Ejecución de los tests de integración El proyecto que contiene los tests automatizados de integración es integration-tests. En él hay una serie de tests que se hacen al WAR de demo y en el que se comprueban los servicios de los plugins que demo incluye. Como estos servicios hacen uso de bases de datos externas, es necesario realizar algún tipo de configuración previa. 12.1 Servicio de base de datos Lo primero es levantar un servicio de base de datos que pueda ser utilizado por los distintos plugins. Una vez el servicio está levantado, hay que configurar en el fichero integration-tests/src/main/resources/org/fao/unredd/functional/functional-test.properties los parámetros de dicha conexión. Por ejemplo: db-url=jdbc:postgresql://192.168.2.107:5432/geoserverdata db-user=geoserver db-password=unr3dd db-test-schema=integration_tests Actualmente esta configuración está apuntando al servidor local ya que así lo requiere el servidor de integración continua Travis. 12.2 Configuración del plugin Feedback En el fichero portal.properties es necesario especificar los parámetros necesarios para que el plugin de feedback pueda encontrar la base de datos: feedback-db-table=integration_tests.comments y pueda enviar emails: feedback-mail-host=smtp.gmail.com feedback-mail-port=587 feedback-mail-username=miusuario@gmail.com feedback-mail-password=mipassword feedback-mail-title=Comentario en portal UNREDD feedback-mail-text=Por favor, visite $url para confirmar el envío. Este fichero portal.properties se encuentra en un directorio de configuración propio de los tests de integración, en integration-tests/test_config. Como para enviar correos es necesario darle al sistema el password de onuredd@gmail.com y no queremos guardarlo en un repositorio de código público, en este portal.properties la propiedad feedback-mail-password tiene el valor $password, que se reemplazará por el valor de la variable de entorno ONUREDDMAILPASSWORD antes de ejecutar los tests. 12.3 Ejecución de los tests Una vez los componentes externos están levantados y el portal configurado, sólo queda ejecutar los tests de integración: mvn verify Sin embargo, pasaremos por la fase package que incluye una operación de optimización. Éstas se pueden evitar desactivando el perfil optimize: mvn -P \!optimize verify Para más información sobre el uso de Maven ver Particularidades de la construcción del proyecto. 13 Manuales Las siguientes secciones incluyen manuales para la creación de las tareas de desarrollo sobre el portal más comunes. 13.1 Hello World Lo primero es crear un nuevo fichero hola-mundo.js en el directorio de módulos. Como contenido podemos definir símplemente: alert("hola mundo"); A continuación registramos el módulo en el fichero portal.properties. En su propiedad client.modules añadimos el nombre de nuestro módulo, que no es otro que el nombre del fichero sin la extensión .js: client.modules=hola-mundo,layers,communication,iso8601,error-management,map,banner,toolbar,time-slide Al recargar el portal nos encontraremos con el mensaje “hola mundo” nada más empezar. 13.2 Añadir elementos a la interfaz Se crea el módulo de la forma habitual, creando el fichero en el directorio de módulos y añadiendo el módulo como dependencia al pom.xml del proyecto. A continuación se debe elegir en qué punto de la página se quiere añadir el botón. En este caso queremos añadirlo a la barra de herramientas principal. Para ello tenemos que obtener el objeto div de dicha barra, el cual nos lo proporciona el módulo toolbar, que importaremos como dependencia: define([ "toolbar" ], function(toolbar) { }); Si observamos el módulo toolbar, podemos ver que devuelve un objeto jQuery con el div y que sólo tenemos que acceder al objeto toolbar para acceder al div: return divToolbar En este punto podríamos realizar una prueba para comprobar que tenemos una referencia valida al div. El siguiente código hace invisible el div de la barra de herramientas: define([ "toolbar" ], function(toolbar) { toolbar.hide(); }); Si hemos hecho todos los pasos correctamente, veremos que la barra de herramientas no aparece, ya que la hemos escondido en nuestro módulo. Lo único que queda por hacer es reemplazar el código de prueba anterior por otro que cree un botón. Esto lo podemos hacer creando un tag <button> con jQuery: define([ "toolbar", "jquery" ], function(toolbar, $) { var aButton = $("<button/>").attr("id", "miboton").addClass("blue_button").html("púlsame"); aButton.appendTo(toolbar); aButton.click(function() { alert("boton pulsado"); }); }); Nótese que, como queremos utilizar jQuery, tenemos que declararla como un módulo y definirla como parámetro $ en la función de inicialización. Por último, podemos añadir un fichero CSS para estilar dicho botón y añadirle un margen, por ejemplo: #miboton { margin: 12px; } 13.3 Añadir una botonera Este manual pretende añadir un botón al portal creando un módulo botonera que facilite la tarea en lo sucesivo. Así pues, el proceso será similar, con la diferencia que en lugar de añadir un botón, vamos a añadir un objeto div: define([ "jquery", "layout" ], function($, layout) { var botonera = $("<div/>").attr("style", "position:absolute;top:6px; left:7em; z-index:2000") botonera.appendTo(layout["map"]); }); En el código anterior podemos observar cómo se crea un objeto div, se estila y se añade al espacio reservado para el mapa por el módulo layout. Esto último quiere decir que la botonera estará sobre el mapa. A continuación debemos de dar la posibilidad a otros módulos para que añadan elementos a dicha botonera. Existen dos maneras: escuchando un mensaje o devolviendo un objeto con un método que añada el botón cuando es invocado. En este manual veremos esta última posibilidad. Para ello el módulo deberá devolver un objeto {} con una propiedad que sea una función: define([ "jquery", "layout" ], function($, layout) { var botonera = $("<div/>").attr("style", "position:absolute;top:6px; left:8em; z-index:2000") botonera.appendTo(layout["map"]); return { newButton : function(text, callback) { // Añade el botón } }; }); Para utilizar esta funcionalidad, es necesario crear un nuevo módulo que importe el módulo botonera: define([ "botonera" ], function(botonera) { }); En este código, la variable botonera recibida en la función de inicialización del módulo es el valor de retorno de la inicialización del módulo botonera, por lo que es posible hacer llamadas a la propiedad newButton de esta variable: define([ "botonera" ], function(botonera) { botonera.newButton("hola mundo", function() { alert('hola mundo'); }); }); Por último, queda implementar el código de la función newButton, que debe tomar al menos un texto y una función callback que se invocará cuando se pinche en el botón: define([ "jquery", "layout" ], function($, layout) { var botonera = $("<div/>").attr("style", "position:absolute;top:6px; left:7em; z-index:2000") botonera.appendTo(layout["map"]); return { newButton : function(text, callback) { var aButton = $("<button/>").html(text); aButton.appendTo(botonera); aButton.click(callback); } }; }); 13.4 Manejo de eventos El siguiente tutorial muestra cómo a través de los eventos existentes es posible interactuar con la plataforma. Para ello crearemos un módulo consistente en poner a invisible todas las capas (checkbox desactivado) mediante dos pasos típicos: 1. Captura de eventos y recogida de información Para poner invisible las capas se utilizará el evento “layer-visibility”, al que se pasa el identificador de la capa y el valor de visibilidad. El valor de visibilidad es siempre falso, pero además se necesitará la lista de identificadores de todas las capas. Es en este punto es necesario escuchar el evento “add-layer” y guardar la información relevante, los identificadores de las capas: define([ "message-bus" ], function(bus) { var layerIds = []; bus.listen("add-layer", function(event, layerInfo) { layerIds.push(layerInfo.id); }); }); 2. Ejecución del módulo Una vez recopilados todos los ids, sólo queda lanzar el mensaje para cada una de las capas. Esto se hará en respuesta a la pulsación en un botón, utilizando el módulo botonera creado en manuales anteriores: define([ "message-bus", "botonera" ], function(bus, botonera) { var layerIds = []; bus.listen("add-layer", function(event, layerInfo) { layerIds.push(layerInfo.id); }); botonera.newButton("todas invisibles", function() { for (var i = 0; i < layerIds.length; i++) { bus.send("layer-visibility", [layerIds[i], false]); } }); }); Patrón habitual El presente manual es un ejemplo sencillo de un patrón que se repite una y otra vez a lo largo del portal: 1.- Escuchado de eventos y recogida de información 2.- Realización de una acción con la información obtenida de los eventos 13.5 Posición del mapa Cada vez que se quiere añadir un mapa OpenLayers en una página web se comienza escribiendo código en el que creamos el mapa: var map = new OpenLayers.Map("map", { theme : null, projection : new OpenLayers.Projection("EPSG:4326"), units : "m", allOverlays : true, controls : [] }); para luego interactuar con él añadiéndole capas, instalando controles, etc.: map.addControl(new OpenLayers.Control.Navigation({ documentDrag : true, zoomWheelEnabled : true })); map.addControl(new OpenLayers.Control.MousePosition({ prefix : '<a target="_blank" ' + 'href="http://spatialreference.org/ref/epsg/4326/">' + 'EPSG:4326</a> coordinates: ', separator : ' | ', numDigits : 2, emptyString : 'Mouse is not over map.' })); Sin embargo, en el portal de diseminación ya existe un mapa creado. ¿Cuál es la manera de proceder para obtener una referencia a dicho mapa y, por ejemplo, mostrar información sobre las coordenadas que se están navegando? La respuesta es sencilla, símplemente hay que importar el módulo map y obtener la referencia en la función de inicialización: define([ "map", "layout" ], function(map, layout) { var divMap = layout["map"]; var divCoor = $("<div/>").attr("id", "coor"); divMap.append(divCoor); var control = new OpenLayers.Control.MousePosition({ prefix : '<a target="_blank" ' + 'href="http://spatialreference.org/ref/epsg/4326/">' div : divCoor.get(0), separator : ' | ', numDigits : 2, emptyString : 'Mouse is not over map.' }); map.addControl(control); }); 13.6 Lectura de parámetros URL Los parámetros de la URL son procesados por el servidor y retornados como configuración del módulo url-parameters. A su vez, el módulo url-parameters ofrece una función get en su valor de retorno que permite obtener los valores de los parámetros. Así pues, para obtener el valor del parámetro lang podemos crear el siguiente módulo: define([ "url-parameters" ], function(urlParams) { alert(urlParams.get("lang")); }); Como se puede observar, se importa el módulo url-parameters en la variable urlParams y posteriormente se llama a la función urlParams.get pasando como parámetro el nombre del parámetro de la URL cuyo valor queremos obtener. Esta función retornará null en caso de que el parámetro no exista. 13.7 Servicio hola mundo El presente manual muestra cómo crear un nuevo servicio que nos devuelve un mensaje “hola mundo” en texto plano o XML, en función de un parámetro. La primera tarea consiste en modificar el fichero web.xml para añadir un nuevo Servlet: <servlet> <servlet-name>holamundo-servlet</servlet-name> <servlet-class>org.fao.unredd.portal.HolaMundoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>holamundo-servlet</servlet-name> <url-pattern>/holamundo</url-pattern> </servlet-mapping> El código anterior asocia el servlet holamundo-servlet con la URL /holamundo y lo implementa con la clase org.fao.unredd.portal.HolaMundoServlet. Ahora sólo es necesario implementar dicha clase: package org.fao.unredd.portal; import javax.servlet.http.HttpServlet; public class HolaMundoServlet extends HttpServlet{ private static final long serialVersionUID = 1L; } La única particularidad del código javax.servlet.http.HttpServlet. anterior es que el servlet debe extender a El atributo estático serialVersionUID no tiene otro objeto que evitar un warning y es totalmente irrelevante para el portal. Si hemos hecho todo correctamente será posible, previo reinicio del servidor, acceder a la URL http://localhost:8080/unredd-portal/holamundo y obtener un error 405: método no permitido. Nótese que el mensaje es distinto si accedemos a una URL inexistente, como http://localhost:8080/unredd-portal/holamundonoexiste, donde obtenemos un 404: no encontrado. Esto quiere decir que el servlet está bien instalado. Sólo hace falta implementar el método GET, que es el que se está pidiendo el navegador: package org.fao.unredd.portal; import java.io.IOException; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; public class HolaMundoServlet extends HttpServlet{ private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } } Ahora, el servidor debe devolver una página en blanco, pero no debe dar un error. Se llega así al punto en el que leeremos el parámetro y en función de este devolveremos un XML o texto plano: package org.fao.unredd.portal; import java.io.IOException; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; public class HolaMundoServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String outputformat = req.getParameter("outputformat"); resp.setCharacterEncoding("utf-8"); if ("xml".equals(outputformat)) { resp.setContentType("application/xml"); resp.getWriter().write("<response>hola mundo</response>"); } else { resp.setContentType("text/plain"); resp.getWriter().write("hola mundo"); } } } 13.8 Servicio configuración (1 de 2) En este manual se presenta la forma de implementar un servicio para visualizar algunos aspectos de la configuración, en concreto la lista de módulos que componen el cliente. En una segunda parte se mostrará cómo realizar modificaciones a la configuración. Como se prentende mostrar un servicio, es necesario crear un servlet modificando el web.xml: <servlet> <servlet-name>module-list-servlet</servlet-name> <servlet-class>org.fao.unredd.portal.ModuleListServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>module-list-servlet</servlet-name> <url-pattern>/moduleList</url-pattern> </servlet-mapping> y creando el fichero Java correspondiente: package org.fao.unredd.portal; import java.io.IOException; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; public class ModuleListServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } } En este caso, el servlet debe acceder a la propiedad client.modules de portal.properties: [...] info.layerUrl=http://demo1.geo-solutions.it/diss_geoserver/gwc/service/wms client.modules=layers,communication,iso8601,error-management,map,banner,toolbar,time-slider,layer-lis map.centerLonLat=24, -4 [...] Dicha propiedad se puede obtener directamenten via el método getPropertyAsArray de la clase org.fao.unredd.portal.Config. Una instancia de esta clase se puede encontrar en el ServletContext y se puede recuperar así: Config config = (Config) getServletContext().getAttribute("config"); Utilizando la librería net.sf.json se puede codificar la lista de módulos como JSON y devolver el resultado: package org.fao.unredd.portal; import java.io.IOException; import java.io.PrintWriter; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; import net.sf.json.JSONArray; public class ModuleListServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Config config = (Config) getServletContext().getAttribute("config"); JSONArray json = new JSONArray(); String[] modules = config.getPropertyAsArray("client.modules"); for (String moduleName : modules) { json.add(moduleName); } resp.setContentType("application/json"); resp.setCharacterEncoding("utf8"); PrintWriter writer = resp.getWriter(); writer.write(json.toString()); } } 13.9 Servicio configuración (2 de 2) En el manual anterior se muestra un servicio que devuelve un array JSON con los módulos activos en el cliente. En este manual se dará la opción de modificar esta lista de módulos eliminando un módulo mediante una petición web. De nuevo se crea un servlet modificando el web.xml: <servlet> <servlet-name>module-removal-servlet</servlet-name> <servlet-class>org.fao.unredd.portal.ModuleRemovalServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>module-removal-servlet</servlet-name> <url-pattern>/moduleRemoval</url-pattern> </servlet-mapping> y creando el fichero Java correspondiente: package org.fao.unredd.portal; import java.io.FileOutputStream; import java.io.IOException; import java.util.Properties; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; public class ModuleRemovalServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } } En este caso, en lugar de obtener la propiedad client.modules del fichero portal.properties es necesario modificarla, lo cual se consigue fácilmente reescribiendo el fichero completo: [...] info.layerUrl=http://demo1.geo-solutions.it/diss_geoserver/gwc/service/wms client.modules=layers,communication,iso8601,error-management,map,banner,toolbar,time-slider,layer-lis map.centerLonLat=24, -4 [...] Para ello son de utilidad: • la clase java.util.Properties, capaz de leer y escribir ficheros de propiedades • el método getPortalPropertiesFile de la clase org.fao.unredd.portal.Config, que devuelve la ubicación del fichero. Para la lectura de un fichero de propiedades es necesario crear un InputStream que acceda al fichero: Properties properties = new Properties(); FileInputStream inputStream = new FileInputStream( config.getPortalPropertiesFile()); properties.load(inputStream); inputStream.close(); De forma análoga, la escritura requiere de un OutputStream: FileOutputStream outputStream = new FileOutputStream( config.getPortalPropertiesFile()); properties.store(outputStream, null); outputStream.close(); Para la eliminación del módulo, se procederá a convertirlo en el un ArrayList, de fácil modificación, para luego regenerar la lista de elementos: String modules = properties.getProperty("client.modules"); String[] moduleArray = modules.split(","); ArrayList<String> moduleList = new ArrayList<String>(); Collections.addAll(moduleList, moduleArray); moduleList.remove(moduleName); properties.put("client.modules", StringUtils.join(moduleList, ',')); Finalmente el servlet quedaría así: package org.fao.unredd.portal; import import import import import import java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.util.ArrayList; java.util.Collections; java.util.Properties; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; public class ModuleRemovalServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Config config = (Config) getServletContext().getAttribute("config"); String moduleName = req.getParameter("moduleName"); Properties properties = new Properties(); // Lectura del fichero FileInputStream inputStream = new FileInputStream( config.getPortalPropertiesFile()); properties.load(inputStream); inputStream.close(); // Eliminación del módulo String modules = properties.getProperty("client.modules"); String[] moduleArray = modules.split(","); ArrayList<String> moduleList = new ArrayList<String>(); Collections.addAll(moduleList, moduleArray); moduleList.remove(moduleName); properties.put("client.modules", StringUtils.join(moduleList, ',')); // Escritura del fichero FileOutputStream outputStream = new FileOutputStream( config.getPortalPropertiesFile()); properties.store(outputStream, null); outputStream.close(); } } Nótese que no se devuelve ningún contenido pero que en cualquier caso, cuando el código del servlet se ejecuta sin error, al cliente le llegará un código HTML “200 OK” indicando que la operación fue satisfactoria. Comunicación con el cliente El servlet anterior parte de la base de que las peticiones que se hagan van a ser satisfactorias, se va a eliminar el módulo, etc. Pero en la realidad esto no es la norma general. ¿Qué sucede si la petición no incluye el parámetro moduleName? ¿Y si el valor no se corresponde con ninguno de los módulos existentes? ¿Qué pasa si el fichero portal.properties ha sido eliminado? El estándar HTML define una serie de códigos que pueden ayudar en la comunicación de estas condiciones excepcionales: • Ok (200): Ejecución satisfactoria. • Bad Request (400): La petición no pudo ser entendida por el servidor. Aquí se puede indicar que el nombre del módulo no se encontró o que no fue especificado el parámetro. Es posible acompañar el código con un mensaje descriptivo. • Internal server error (500): Adecuado para indicar errores graves, irrecuperables, como un bug en el código o que el fichero portal.properties no existe! La clase org.fao.unredd.portal.ErrorServlet es la encargada de gestionar los errores que se producen en el sistema. La única característica especial que tiene es que está implementada de tal manera que si se lanza una excepción org.fao.unredd.portal.StatusServletException, el código que se pasa como parámetro será el código que se le devuelva al cliente. Además, es posible especificarle a esta instrucción el mensaje que se enviará al cliente. Por ejemplo, en caso de que se desee enviar un código 400 cuando el parámetro moduleName no esté presente se procedería así: if (moduleName == null) { throw new StatusServletException(400, "El parámetro moduleName es obligatorio"); } El segundo parámetro se enviaría codificado en un documento JSON, para que el cliente que realice la llamada pueda leerlo y presentarlo al usuario convenientemente. Así pues, si se accede a la URL http://localhost:8080/unredd-portal/moduleRemoval (sin el parámetro) se obtendrá como resultado un código 400 y el siguiente documento: { "message": "El parámetro moduleName es obligatorio" } Teniendo esto en cuenta, el servlet anterior se podría escribir así: package org.fao.unredd.portal; import import import import import import java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.util.ArrayList; java.util.Collections; java.util.Properties; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; public class ModuleRemovalServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Config config = (Config) getServletContext().getAttribute("config"); String moduleName = req.getParameter("moduleName"); if (moduleName == null) { throw new StatusServletException(400, "El parámetro moduleName es obligatorio"); } Properties properties = new Properties(); // Lectura del fichero try { FileInputStream inputStream = new FileInputStream( config.getPortalPropertiesFile()); properties.load(inputStream); inputStream.close(); } catch (IOException e) { throw new StatusServletException(500, "Error grave en el servidor. Contacte al administrador"); } // Eliminación del módulo String modules = properties.getProperty("client.modules"); String[] moduleArray = modules.split(","); ArrayList<String> moduleList = new ArrayList<String>(); Collections.addAll(moduleList, moduleArray); if (!moduleList.remove(moduleName)) { throw new StatusServletException(400, "El módulo especificado no existe"); } properties.put("client.modules", StringUtils.join(moduleList, ',')); // Escritura del fichero try { FileOutputStream outputStream = new FileOutputStream( config.getPortalPropertiesFile()); properties.store(outputStream, null); outputStream.close(); } catch (IOException e) { throw new StatusServletException(500, "Error grave en el servidor. Contacte al administrador"); } } } Decodificación en el cliente Por último, cabe destacar que el módulo communication.js escucha un evento ajax que permite realizar llamadas a nuestro servidor y que en caso de error leería el atributo message del documento JSON generado y lo mostraría al usuario. El siguiente módulo hace la petición para eliminar el módulo banner cuando se pulsa un botón: define([ "message-bus", "botonera" ], function(bus, botonera) { botonera.newButton("remove banner", function() { bus.send("ajax", { url : "moduleRemoval?moduleName=banner", success : function(indicators, textStatus, jqXHR) { alert("módulo eliminado con éxito"); }, errorMsg : "No se pudo eliminar el módulo" }); }); }); La primera vez debe funcionar correctamente, pero la segunda debe fallar porque el módulo banner ya no está presente. Como la comunicación se realiza via el módulo communication con el evento ajax, en caso de error el propio módulo lee el mensaje y lo muestra al usuario. 13.10 Conexión a base de datos Conexión a base de datos en Java Cuando queremos conectar a la base de datos desde un servicio Java tenemos que utilizar la API JDBC (Java DataBase Connectivity) de Java. En general, el código para conectar a una base de datos en Java es el siguiente: Class.forName("org.postgresql.Driver"); Connection connection = DriverManager.getConnection( "jdbc:postgresql://hostname:port/dbname","username", "password"); ... connection.close(); Warning: En el código anterior estamos conectando a una base de datos PostgreSQL, para lo cual instanciamos el driver org.postgresql.Driver y conectamos usando la URL propia de PostgreSQL jdbc:postgresql://hostname:port. Estos dos aspectos cambiarán en función del tipo de base de datos a la que estemos conectando. Primero, instanciamos el driver para que se autoregistre en el DriverManager y poder invocar después el método getConnection para obtener la conexión. Por último, es necesario cerrar la conexión. Por medio del objeto de tipo Connection podremos obtener instancias de Statement, con las que se pueden enviar instrucciones SQL al servidor de base de datos. Conexión a base de datos desde una aplicación web Partimos de un servicio configurado de esta manera en el descriptor de despliegue: <!-- database example --> <servlet> <servlet-name>example-db</servlet-name> <servlet-class>org.fao.unredd.portal.ExampleDBServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>example-db</servlet-name> <url-pattern>/example-db</url-pattern> </servlet-mapping> y que implementará su funcionalidad en el método GET: package org.fao.unredd.portal; import java.io.IOException; import import import import javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; public class ExampleDBServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //... } } Aunque sería posible poner el código anterior en el método doGet, no es recomendable debido a que la creación de una conexión es siempre un proceso costoso. Configuración conexión en el descriptor de despliegue Para evitar crear una conexión cada vez, tendremos que configurar el contenedor de aplicaciones, Tomcat en este ejemplo, para que gestione las conexiones por nosotros. Para ello, tenemos que darle a Tomcat la información necesaria para conectar modificando dos ficheros. El primero es el fichero context.xml que existe en el directorio de configuración del servidor conf. Ahí declararemos un recurso llamado “jdbc/mis-conexiones” que incluirá todos los datos necesarios para conectar: url, usuario, etc.: <Resource name="jdbc/mis-conexiones" auth="Container" type="javax.sql.DataSource" driverClassName="org.postgresql.Driver" url="jdbc:postgresql://192.168.0.18:5432/geoserverdat username="nfms" password="unr3dd" maxActive="20" maxIdle="10" maxWait="-1" /> El otro fichero a modificar es el descriptor de despliegue web-fragment.xml del plugin que estamos desarrollando (ver Estructura proyectos plugin), donde añadiremos una referencia al recurso anterior, “jdbc/mis-conexiones”: <resource-ref> <description>Application database</description> <res-ref-name>jdbc/mis-conexiones</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> Note: Al ejecutar una aplicación en Tomcat desde Eclipse se crea un proyecto “Servers”, que contiene una entrada para el servidor que estamos utilizando y que en el caso de Tomcat incluye los ficheros de configuración, entre otros el fichero context.xml donde configuramos el recurso. La manera más sencilla de acceder al fichero context del servidor es a través de dicho proyecto. Una vez estos ficheros han sido modificados ya no tenemos que preocuparnos de realizar la conexión porque Tomcat las gestiona por nosotros. Pero, ¿cómo podemos obtener una de estas conexiones gestionadas por Tomcat? El código Java cambia ligeramente, ya que ahora se obtiene un objeto de tipo java.sql.DataSource que es el que nos proporciona las conexiones: InitialContext context; DataSource dataSource; try { context = new InitialContext(); dataSource = (DataSource) context .lookup("java:/comp/env/jdbc/mis-conexiones"); } catch (NamingException e) { throw new ServletException("Problema en la configuración"); } try { Connection connection = dataSource.getConnection(); // ... connection.close(); } catch (SQLException e) { throw new ServletException("No se pudo obtener una conexión"); } try { context.close(); } catch (NamingException e) { // ignore } Si sutituímos la línea que contiene los puntos suspensivos por código que haga algo más interesante con la conexión, podemos devolver un JSON con el array de nombres que haya en una tabla: package org.fao.unredd.portal; import import import import import import java.io.IOException; java.sql.Connection; java.sql.ResultSet; java.sql.SQLException; java.sql.Statement; java.util.ArrayList; import import import import import import import javax.naming.InitialContext; javax.naming.NamingException; javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; javax.sql.DataSource; import net.sf.json.JSONSerializer; public class ExampleDBServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { InitialContext context; DataSource dataSource; try { context = new InitialContext(); dataSource = (DataSource) context .lookup("java:/comp/env/jdbc/mis-conexiones"); } catch (NamingException e) { throw new ServletException("Problema en la configuración"); } ArrayList<String> provincias = new ArrayList<String>(); try { Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement(); ResultSet result = statement .executeQuery("SELECT name_1 FROM gis.arg_adm1"); while (result.next()) { provincias.add(result.getString("name_1")); } resp.setContentType("application/json"); JSONSerializer.toJSON(provincias).write(resp.getWriter()); connection.close(); } catch (SQLException e) { throw new ServletException("No se pudo obtener una conexión", e); } try { context.close(); } catch (NamingException e) { throw new ServletException("No se pudo liberar el recurso"); } } } La clase DBUtils Conexiones existentes Como se puede ver en http://nfms4redd.org/tmp/ref/install/portal.html, el portal incorpora ya una conexión a una base de datos que se deberá configurar a nivel del contenedor de aplicaciones (Tomcat). La referencia a esa conexión está configurada en el web-fragment.xml de core, que todo plugin debe incluir como dependencia (y por tanto, todo plugin puede utilizar): <resource-ref> <description>Application database</description> <res-ref-name>jdbc/unredd-portal</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> Como se puede observar, el nombre es “jdbc/unredd-portal” por lo que con esta información, y usando la clase DBUtils vista anteriormente, sería posible reescribir el servlet anterior de la siguiente manera y sin tocar ningún fichero de configuración: package org.fao.unredd.portal; import import import import import import java.io.IOException; java.sql.Connection; java.sql.ResultSet; java.sql.SQLException; java.sql.Statement; java.util.ArrayList; import import import import import import import javax.naming.InitialContext; javax.naming.NamingException; javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; javax.sql.DataSource; import net.sf.json.JSONSerializer; public class ExampleDBServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { final ArrayList<String> provincias = new ArrayList<String>(); try { DBUtils.processConnection("unredd-portal", new DBUtils.DBProcessor() { @Override public void process(Connection connection) throws SQLException { Statement statement = connection.createStatement(); ResultSet result = statement .executeQuery("SELECT name_1 FROM gis.arg_adm while (result.next()) { provincias.add(result.getString("name_1")); } } }); } catch (PersistenceException e) { throw new ServletException("No se pudo obtener una conexión", e); } resp.setContentType("application/json"); JSONSerializer.toJSON(provincias).write(resp.getWriter()); } } 13.11 Cómo crear un nuevo plugin Cuando queremos realizar una implementación que se pueda utilizar fácilmente en portales existentes hay que empaquetar las funcionalidades en cuestión en un plugin. Creación del proyecto para el plugin Para ello hay que crear un nuevo proyecto con Maven. Escribiendo desde la línea de comandos: $ mvn archetype:generate Dicho comando inicia un asistente que nos permitirá crear un proyecto fácilmente. Primero nos preguntará por el tipo de proyecto y versión del plugin de Maven. Nos valen los valores por defecto por lo que sólo pulsaremos INTRO un par de veces: Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 497: INTRO Choose org.apache.maven.archetypes:maven-archetype-quickstart version: 1: 1.0-alpha-1 2: 1.0-alpha-2 3: 1.0-alpha-3 4: 1.0-alpha-4 5: 1.0 6: 1.1 Choose a number: 6: INTRO A continuación nos preguntará por el groupId y artifactId, que no es otra cosa que una manera de identificar el plugin. En este ejemplo usaremos respectivamente “org.fao.unredd” y “holamundo”: Define value for property 'groupId': : org.fao.unredd Define value for property 'artifactId': : holamundo Luego nos preguntará versión y paquete de Java que queremos crear. Podemos aceptar las opciones por defecto pulsando INTRO en cada caso: Define value for property 'version': Define value for property 'package': 1.0-SNAPSHOT: : INTRO org.fao.unredd: : INTRO Por último nos pedirá confirmación. Pulsaremos INTRO de nuevo: Confirm properties configuration: groupId: org.fao.unredd artifactId: holamundo version: 1.0-SNAPSHOT package: org.fao.unredd Y: : [INFO] ---------------------------------------------------------------------------[INFO] Using following parameters for creating project from Old (1.x) Archetype: maven-archetype-quic [INFO] ---------------------------------------------------------------------------[INFO] Parameter: groupId, Value: org.fao.unredd [INFO] Parameter: packageName, Value: org.fao.unredd [INFO] Parameter: package, Value: org.fao.unredd [INFO] Parameter: artifactId, Value: holamundo [INFO] Parameter: basedir, Value: /tmp [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] project created from Old (1.x) Archetype in dir: /tmp/holamundo [INFO] -----------------------------------------------------------------------[INFO] BUILD SUCCESS [INFO] [INFO] [INFO] [INFO] [INFO] -----------------------------------------------------------------------Total time: 4:16.288s Finished at: Wed Nov 05 10:10:44 CET 2014 Final Memory: 15M/108M ------------------------------------------------------------------------ Tras este proceso, Maven nos reporta BUILD SUCCESS, que quiere decir que el proyecto fue creado con éxito y está en un directorio con el mismo nombre que el artifactId, es decir, “holamundo”. Configuración en Eclipse Para empezar a escribir código, tendremos que importar el proyecto en algún entorno de desarrollo, como Eclipse. Para ello hacemos clic con el botón derecho en el Project Explorer > Import > Import... En el diálogo que aparece hay que decirle a Eclipse que el proyecto que queremos importar es un proyecto Maven. Así, habrá que seleccionar Maven > Existing Maven Projects A continuación hay que seleccionar el directorio que Maven acaba de crear y finalizar el asistente. Para finalizar la importación en eclipse tendremos que crear el directorio src/main/resources que no viene creado por defecto pero que es donde se incluirán todos los desarrollos para la parte cliente (ver Estructura proyectos plugin). Una vez creado habrá que hacer clic derecho en el proyecto y seleccionar Maven > Update project, tras lo cual src/main/resources aparecerá como un directorio de código: Desarrollo de un módulo Ahora que tenemos el proyecto en Eclipse podemos crear un módulo hola mundo. Los módulos tienen que estar en el directorio nfms/modules en src/main/resources por lo que tendremos que crear ese directorio. Dentro de ese directorio podemos crear el módulo, como se ve en la imagen: Reutilización del módulo Por último, queremos que nuestro plugin se incluya en alguna aplicación, por ejemplo demo. Esto es tan fácil como incluir nuestro plugin como dependencia de demo. Para ello abrimos el pom.xml de demo e incluimos una sección <dependency> adicional con los datos que introdujimos en nuestro plugin al inicio del manual: Ahora sólo queda ejecutar demo en un servidor Tomcat y ver el resultado: Warning: Es posible que el plugin no aparezca inicialmente por problemas de refresco, se recomienda clicar con el botón derecho uno de los proyectos y seleccionar Maven > Update project seleccionando en el diálogo que aparece todos los proyectos implicados (plugins y aplicación). 13.12 Cómo crear una nueva aplicación Creación del proyecto Cuando el objetivo es crear una aplicación que agrupe uno o más plugins existentes tenemos que crear un tipo de proyecto distinto (ver Estructura proyectos aplicación para información sobre los artefactos que tiene que tener un proyecto tal). Al igual que en el caso de crear un nuevo plugin, lo primero es crear un nuevo proyecto maven. El proceso es idéntico a Creación del proyecto para el plugin sólo que utilizaremos “mi-app” como artifactId. La configuración de dicho proyecto en Eclipse es también realizada de forma idéntica al caso de los plugins: Configuración en Eclipse. Selección de los plugins que componen la aplicación Una vez el proyecto está creado, es necesario especificar los plugins que van a utilizarse en la aplicación. Para ello habrá que modificar el fichero pom.xml incluyendo los elementos <dependency> de core, el cargador de plugins, y de base, plugin principal: <dependency> <groupId>org.fao.unredd</groupId> <artifactId>core</artifactId> <version>3.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.fao.unredd</groupId> <artifactId>base</artifactId> <version>3.1-SNAPSHOT</version> </dependency> Además, para que Maven pueda descargar estas dependencias, hay que especificar algunos repositorios de librerías: <pluginRepositories> <pluginRepository> <id>nfms4redd</id> <name>nfms4redd maven repository</name> <url>http://maven.nfms4redd.org/</url> </pluginRepository> </pluginRepositories> <repositories> <repository> <id>osgeo</id> <name>Open Source Geospatial Foundation Repository</name> <url>http://download.osgeo.org/webdav/geotools/</url> </repository> <repository> <id>nfms4redd</id> <name>nfms4redd maven repository</name> <url>http://maven.nfms4redd.org/</url> </repository> <repository> <id>EclipseLink</id> <url>http://download.eclipse.org/rt/eclipselink/maven.repo</url> </repository> <repository> <id>geosolutions</id> <name>GeoSolutions public maven repository</name> <url>http://maven.geo-solutions.it/</url> </repository> </repositories> El fichero pom.xml quedaría de la siguiente manera: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instan xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0 <modelVersion>4.0.0</modelVersion> <groupId>org.fao.unredd</groupId> <artifactId>mi-app</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>mi-app</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <pluginRepositories> <pluginRepository> <id>nfms4redd</id> <name>nfms4redd maven repository</name> <url>http://maven.nfms4redd.org/</url> </pluginRepository> </pluginRepositories> <repositories> <repository> <id>osgeo</id> <name>Open Source Geospatial Foundation Repository</name> <url>http://download.osgeo.org/webdav/geotools/</url> </repository> <repository> <id>nfms4redd</id> <name>nfms4redd maven repository</name> <url>http://maven.nfms4redd.org/</url> </repository> <repository> <id>EclipseLink</id> <url>http://download.eclipse.org/rt/eclipselink/maven.repo</url> </repository> <repository> <id>geosolutions</id> <name>GeoSolutions public maven repository</name> <url>http://maven.geo-solutions.it/</url> </repository> </repositories> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.fao.unredd</groupId> <artifactId>core</artifactId> <version>3.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.fao.unredd</groupId> <artifactId>base</artifactId> <version>3.1-SNAPSHOT</version> </dependency> </dependencies> </project> Ejecución de la aplicación desde eclipse Para la ejecución del proyecto como aplicación web dentro de Eclipse tenemos que realizar dos configuraciones adicionales. Lo primero es configurar el proyecto para que Eclipse entienda que es una aplicación web. Para ello hay que modificar el elemento packaging del fichero mi-app/pom.xml como se puede ver en el listado anterior correspondiente al pom.xml, estableciendo el valor a “war”. Tras editar el fichero habrá que clicar en el proyecto con botón derecho y seleccionar Maven > Update project. A continuación es necesario proporcionar a la aplicación un directorio de proporciona a la aplicación información sobre las capas del mapa, etc. el que hay en demo/src/main/webapp/WEB-INF/default_config mi-app/src/main/webapp/WEB-INF/default_config. configuración, que Podemos tomar y copiarlo en Por último, para ejecutar la aplicación tendremos que operar como se muestra con demo en el punto Ejecución del portal en tomcat desde Eclipse, pero con el proyecto mi-app que acabamos de crear. Empaquetado Warning: Para que el proceso funcione es necesario que exista el descriptor de despliegue de aplicaciones JEE, el fichero src/main/webapp/WEB-INF/web.xml. Bastaría con crear ese fichero con el siguiente contenido: <?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web</web-app> Para realizar el empaquetado tenemos que ejecutar el comando mvn package en el directorio mi-app. Esto también se puede hacer desde Eclipse haciendo clic con el botón derecho en el proyecto mi-app y seleccionando Run As > Maven Build. En la ventana que aparece hay que especificar “package” en “Goals”, como se puede ver en la siguiente imagen: Al pinchar en el botón Run, Maven se ejecutará y mostrará por la consola el resultado. Cuando el proceso se termina con éxito se obtiene el fichero .war en el directorio target del proyecto y un mensaje similar a éste: [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] --- maven-war-plugin:2.2:war (default-war) @ mi-app --Packaging webapp Assembling webapp [mi-app] in [/home/fergonco/temp/howtoworkspace/mi-app/target/mi-app-1.0-SNA Processing war project Copying webapp resources [/home/fergonco/temp/howtoworkspace/mi-app/src/main/webapp] Webapp assembled in [172 msecs] Building war: /home/fergonco/temp/howtoworkspace/mi-app/target/mi-app-1.0-SNAPSHOT.war WEB-INF/web.xml already added, skipping -----------------------------------------------------------------------BUILD SUCCESS -----------------------------------------------------------------------Total time: 3.299s Finished at: Thu Nov 06 11:40:09 CET 2014 [INFO] Final Memory: 12M/172M [INFO] ------------------------------------------------------------------------ Warning: Si la aplicación tiene como dependencia un plugin que hemos desarrollado nosotros, es necesario que dicho plugin esté disponible para Maven, lo cual se consigue ejecutando el goal “install” en dicho plugin. Empaquetado con optimización Cuando una aplicación tiene muchos módulos y librerías Javascript, hojas de estilo CSS, etc. la carga puede ser un poco lenta. Para acelerar esto se puede configurar Maven para que realice un proceso de optimización y combine todos estos ficheros en uno sólo. Primero, hay que introducir la siguiente sección en el pom.xml de mi-app tras la sección <dependencies></dependencies>: <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.8</version> <executions> <execution> <id>unpack-dependencies</id> <phase>prepare-package</phase> <goals> <goal>unpack-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/requirejs </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.fao.unredd</groupId> <artifactId>jwebclient-analyzer-maven-plugin</artifactId> <version>4.0.1</version> <executions> <execution> <id>generate-buildconfig</id> <phase>prepare-package</phase> <goals> <goal>generate-buildconfig</goal> </goals> <configuration> <mainTemplate>${project.build.directory}/requirejs/ma <webClientFolder>${project.build.directory}/requirejs <buildconfigOutputPath>${project.build.directory}/bui <mainOutputPath>${project.build.directory}/requirejs/ </configuration> </execution> </executions> </plugin> <plugin> <groupId>ro.isdc.wro4j</groupId> <artifactId>wro4j-maven-plugin</artifactId> <version>1.7.6</version> <executions> <execution> <phase>prepare-package</phase> <goals> <goal>run</goal> </goals> </execution> </executions> <configuration> <wroManagerFactory>ro.isdc.wro.maven.plugin.manager.factory.Configura <extraConfigFile>${basedir}/src/main/config/wro.properties</extraConf <targetGroups>portal-style</targetGroups> <minimize>true</minimize> <contextFolder>${basedir}/target/requirejs/nfms/</contextFolder> <destinationFolder>${basedir}/src/main/webapp/optimized/</destination <wroFile>${basedir}/src/main/config/wro.xml</wroFile> </configuration> </plugin> <plugin> <groupId>com.github.bringking</groupId> <artifactId>requirejs-maven-plugin</artifactId> <version>2.0.4</version> <executions> <execution> <phase>prepare-package</phase> <goals> <goal>optimize</goal> </goals> </execution> </executions> <configuration> <!-- optional path to a nodejs executable --> <!--<nodeExecutable> --> <!--/opt/nodejs/node --> <!--</nodeExecutable> --> <!-- path to optimizer json config file --> <configFile>${project.build.directory}/buildconfig.js</configFile> <fillDepsFromFolder>${project.build.directory}/requirejs/nfms/modules <!-- optional path to optimizer executable --> <!--<optimizerFile> --> <!--${basedir}/src/main/scripts/r.js --> <!--</optimizerFile> --> <!-- optional parameters to optimizer executable --> <optimizerParameters> <parameter>optimize=uglify</parameter> <!--<parameter>baseUrl=${baseDir}</parameter> --> </optimizerParameters> <!-- Whether or not to process configFile with maven filters. If you use this option, some options in your configFile must resolve paths (see below) --> <filterConfig> true </filterConfig> <!-- Skip requirejs optimization if true --> <skip> false </skip> </configuration> </plugin> </plugins> </build> Esta configuración hace referencia a dos ficheros existentes en el directorio src/main/config, wro.properties y wro.xml. El contenido de wro.properties será: preProcessors=cssDataUri,cssImport,semicolonAppender,cssMinJawr postProcessors= Mientras que para wro.xml pondremos: <?xml version="1.0" encoding="UTF-8"?> <groups xmlns="http://www.isdc.ro/wro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.isdc.ro/wro wro.xsd"> <group name="portal-style"> <css>/modules/**.css</css> <css>/styles/**.css</css> </group> </groups> Una vez realizada esta configuración, podemos generar el WAR de nuevo. Aparentemente este WAR es igual que el anterior, pero a diferencia de aquél, justo antes de empaquetar se habrán generado dos ficheros: src/main/webapp/optimized/portal.js y src/main/webapp/optimized/portal-style.css, que incluyen respectivamente todo el código Javascript y todos los estilos de los plugins usados por la aplicación. Cuando despleguemos tal WAR, podremos seleccionar poniendo la variable de entorno MINIFIED_JS a “true” el modo optimizado, que cargará el portal bastante más rápido. 13.13 Cómo dar licencia libre a un plugin Una vez hemos realizado nuestro plugin y tenemos claro que queremos publicarlo con una licencia para que la gente lo reutilice de forma libre, tenemos que seguir los siguientes pasos: 1. Elegir una licencia libre 2. Aplicar la licencia a nuestro proyecto Elegir una licencia libre Existen muchísimas licencias de software libre por lo que para este tutorial nos limitaremos al ámbito de las más utilizadas. Además, para facilitar la aplicación de la licencia nos limitaremos también a las soportadas por el maven-license-plugin. Para obtener un listado de las licencias basta con ejecutar el objetivo license-list del plugin Maven license: $ mvn license:license-list [INFO] Scanning for projects... [INFO] [INFO] -----------------------------------------------------------------------[INFO] Building Maven Stub Project (No POM) 1 [INFO] -----------------------------------------------------------------------[INFO] [INFO] --- license-maven-plugin:1.8:license-list (default-cli) @ standalone-pom --[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform depen [INFO] Available licenses : * * * * * * * * * * * * * * * agpl_v3 apache_v2 bsd_2 bsd_3 cddl_v1 epl_only_v1 epl_v1 eupl_v1_1 fdl_v1_3 gpl_v1 gpl_v2 gpl_v3 lgpl_v2_1 lgpl_v3 mit [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] : : : : : : : : : : : : : : : GNU Affero General Public License (AGPL) version 3.0 Apache License version 2.0 BSD 2-Clause License BSD 3-Clause License COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 Eclipse Public License - v 1.0 Eclipse Public + Distribution License - v 1.0 European Union Public License v1.1 GNU Free Documentation License (FDL) version 1.3 GNU General Public License (GPL) version 1.0 GNU General Public License (GPL) version 2.0 GNU General Public License (GPL) version 3.0 GNU General Lesser Public License (LGPL) version 2.1 GNU General Lesser Public License (LGPL) version 3.0 MIT-License -----------------------------------------------------------------------BUILD SUCCESS -----------------------------------------------------------------------Total time: 2.228s Finished at: Thu Mar 05 06:46:17 CET 2015 Final Memory: 10M/144M ------------------------------------------------------------------------ Cada licencia tiene unas particularidades, pero está fuera del ámbito de este tutorial analizar estas diferencias. Para el siguiente punto, aplicaremos a nuestro proyecto la licencia GPLv3 (GNU General Public License (GPL) version 3.0), que es la misma que tiene el portal de FAO. Aplicar la licencia a nuestro proyecto Ahora que tenemos nuestro proyecto y sabemos la licencia que queremos que tenga, GPLv3, ¿cómo se la aplicamos? Para aplicar esta licencia tenemos que incluir en la raíz de nuestro proyecto un fichero LICENSE.txt con el texto de la licencia y en cada fichero de código, una cabecera similar a la siguiente: /* * * * * * * * * * * * * * * * NOMBRE DEL PROYECTO Copyright (C) AÑO ORGANIZACIÓN This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * */ Esto, que sería bastante tedioso de realizar a mano, lo hace de forma automática el plugin license de Maven, pero para que pueda llevar estas acciones a cabo es necesario configurarlo en el pom.xml con los datos propios de nuestro proyecto, que se usarán para personalizar el texto de la cabecera. Vamos a suponer que partimos de un pom.xml tan sencillo como éste: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instan xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>mi.organizacion</groupId> <artifactId>pluginParaX</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>PluginParaX</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> </project> Para configurar nuestro plugin tendremos que añadir una sección build/plugins, dentro de la cual pondremos la configuración del plugin license: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instan xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>mi.organizacion</groupId> <artifactId>pluginParaX</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>PluginParaX</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Aquí va la configuración del plugin license --> </plugins> </build> </project> Para la configuración del plugin usaremos algo similar a este elemento plugin. Nótese que en un fichero XML, los símbolos <!-- y --> sirven para abrir y cerrar comentarios: <plugin> <!-- Identificación del plugin license --> <groupId>org.codehaus.mojo</groupId> <artifactId>license-maven-plugin</artifactId> <!-- Configuración para el plugin anterior --> <configuration> <!-- Año en el que se publica el plugin --> <inceptionYear>2015</inceptionYear> <!-- Organización que publica el código --> <organizationName>FAO</organizationName> <!-- Nombre del proyecto como aparecerá en la licencia --> <projectName>PluginParaX</projectName> <!-- licencia escogida de la lista del punto anterior --> <licenseName>gpl_v3</licenseName> <!-directorios donde se encuentran los ficheros de código cuya cabecera queremos editar --> <roots> <root>src/main/java</root> <root>src/main/resources/nfms/modules</root> <root>src/main/resources/nfms/styles</root> </roots> <!-Patrones que indentifican los ficheros cuya cabecera queremos editar en los directorios especificados por "roots" --> <includes> <include>*.java</include> <include>*.js</include> <include>*.css</include> </includes> </configuration> </plugin> Finalmente, insertando el elemento plugin anterior en el pom.xml, el fichero quedaría de la siguiente manera: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instan xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>mi.organizacion</groupId> <artifactId>pluginParaX</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>PluginParaX</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <!-- Identificación del plugin license --> <groupId>org.codehaus.mojo</groupId> <artifactId>license-maven-plugin</artifactId> <!-- Configuración para el plugin anterior --> <configuration> <!-- Año en el que se publica el plugin --> <inceptionYear>2015</inceptionYear> <!-- Organización que publica el código --> <organizationName>FAO</organizationName> <!-- Nombre del proyecto como aparecerá en la licencia --> <projectName>PluginParaX</projectName> <!-- licencia escogida de la lista del punto anterior --> <licenseName>gpl_v3</licenseName> <!-directorios donde se encuentran los ficheros de código cuya cabecera queremos editar. Especificaremos los directorios donde se encuentran los ficheros Java y los módulos Javascript. --> <roots> <root>src/main/java</root> <root>src/main/resources/nfms/modules</root> <root>src/main/resources/nfms/styles</root> </roots> <!-Patrones que indentifican los ficheros cuya cabecera queremos editar en los directorios especificados por "roots". Especificaremos ficheros Java, Javascript y las hojas de estilo CSS. --> <includes> <include>*.java</include> <include>*.js</include> <include>*.css</include> </includes> </configuration> </plugin> </plugins> </build> </project> Una vez la configuración está realizada, ya sólo queda realizar las dos acciones necesarias: añadir el texto de la licencia y las cabeceras. Para añadir la licencia podemos ejecutar el objetivo update-project-license: $ mvn license:update-project-license [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for mi.organizacion:plugi [WARNING] 'build.plugins.plugin.version' for org.codehaus.mojo:license-maven-plugin is missing. @ lin [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed proj [WARNING] [INFO] [INFO] -----------------------------------------------------------------------[INFO] Building PluginParaX 1.0-SNAPSHOT [INFO] -----------------------------------------------------------------------[INFO] [INFO] --- license-maven-plugin:1.8:update-project-license (default-cli) @ PluginParaX --[INFO] Will create or update license file [gpl_v3] to /tmp/pluginParaX/LICENSE.txt [INFO] -----------------------------------------------------------------------[INFO] BUILD SUCCESS [INFO] -----------------------------------------------------------------------[INFO] Total time: 1.738s [INFO] Finished at: Thu Mar 05 08:48:02 CET 2015 [INFO] Final Memory: 9M/144M [INFO] ------------------------------------------------------------------------ que añadirá el fichero LICENSE.txt en la raíz del proyecto. Y por último, para añadir las cabeceras, usaremos el objetivo update-file-header: $ mvn license:update-file-header [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for mi.organizacion:Plugi [WARNING] 'build.plugins.plugin.version' for org.codehaus.mojo:license-maven-plugin is missing. @ lin [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed proj [WARNING] [INFO] [INFO] -----------------------------------------------------------------------[INFO] Building PluginParaX 1.0-SNAPSHOT [INFO] -----------------------------------------------------------------------[INFO] [INFO] --- license-maven-plugin:1.8:update-file-header (default-cli) @ PluginParaX --[INFO] Will search files to update from root /tmp/pluginParaX/src/main/java [INFO] Will search files to update from root /tmp/pluginParaX/src/main/resources/nfms/modules [INFO] Scan 4 files header done in 27.798ms. [INFO] * add header on 4 files. [INFO] -----------------------------------------------------------------------[INFO] BUILD SUCCESS [INFO] [INFO] [INFO] [INFO] [INFO] -----------------------------------------------------------------------Total time: 1.874s Finished at: Thu Mar 05 08:50:36 CET 2015 Final Memory: 10M/144M ------------------------------------------------------------------------ Por último, sólo nos queda poner nuestro plugin en un lugar accesible para que lo puedan encontrar otros desarrolladores. 13.14 Publicación de un plugin en un repositorio Maven Para que los plugins desarrollados puedan ser reutilizados en otros desarrollos, ya sean propios o de terceras partes, es necesario subirlos a un repositorio Maven. Para ello modificaremos dos ficheros: • El pom.xml de nuestro plugin • El fichero de configuración settings.xml de Maven Un repositorio Maven es una estructura de directorios que contiene los plugins y sus dependencias organizados por groupId y artifactId. El repositorio principal de Maven se encuentra en http://central.maven.org/maven2/ y contiene las librerías de uso general. Para los plugins y las librerías más específicas del portal REDD, FAO pone a disposición el servidor maven.nfms4redd.org. Es este servidor el que podemos utilizar para subir nuestro plugin a un sitio accesibile. Si hemos desarrollado un plugin, seguro que tenemos Maven ya configurado para que se descargue las dependencias del portal del repositorio de FAO. Pero ahora habrá que configurarlo para que, además de descargar los plugins, pueda subirlos. Para subir los plugins al repositorio de FAO hay que utilizar el servicio ftp://maven.nfms4redd.org/repo. Esto se configura en el pom.xml de nuestro plugin mediante dos elementos. El primero es un elemento dentro de <build> que dice a Maven que vamos a acceder por FTP: <build> <extensions> <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ftp</artifactId> <version>2.3</version> </extension> </extensions> </build> El segundo le dice a Maven la URL del servidor: <distributionManagement> <repository> <id>nfms4redd</id> <url>ftp://maven.nfms4redd.org/repo</url> <uniqueVersion>false</uniqueVersion> </repository> </distributionManagement> Como no queremos compartir el usuario y contraseña para acceder al FTP, lo que hacemos es especificar sólo un identificador, en este caso nfms4redd, al que haremos referencia desde el fichero de configuración de Maven settings.xml para asignar a ese id el usuario y contraseña. El fichero settings.xml se encuentra en el directorio .m2 del “HOME” del usuario: En dicho fichero, crearemos un elemento server con el mismo identificador que el especificado en el pom.xml del plugin. Y en ese elemento pondremos el usuario y contraseña. <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <servers> <server> <id>nfms4redd</id> <username>XXXXX</username> <password>XXXXX</password> </server> </servers> </settings> Una vez la configuración está terminada, es necesario pedir a Maven que ejecute la tarea (goal) deploy. Warning: Es posible que la ejecución de Maven falle si se ejecuta desde Eclipse y tenemos configurado sólo un JRE y no un JDK. En tal caso la solución es simple: instalar un JDK y utilizarlo por defecto.