Database MessageSource: obtener los literales de una base de datos
0. Índice de
contenidos.
1. Entorno
Este tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil Mac Book Pro 17″ (2,6 Ghz Intel Core i7, 8 GB DDR3)
- Sistema Operativo: Mac OS X Snow Leopard 10.6.4
- Spring 3.1.0.RC1
- Mybatis 3.0.4
2. Introducción
Lo más normal a la hora de internacionalizar aplicaciones es contar con ficheros de propiedades que almacenan los literales que se utilizan, los famosos ResourceBundle, que nos permite tener n ficheros cada uno del idioma que queramos visualizar. Este es el funcionamiento habitual a la hora de hacer i18n. En este tutorial vamos a ver como obtener estos literales almacenados en nuestra propia base de datos y seguir utilizando el estándar de Spring para no tener que cambiar el código de la aplicación, tanto para JSP’s cuando utilizamos la etiqueta <spring:message code=»code»/> o cuando utilizamos la interfaz MessageSource en nuestro código Java, así como la forma de parametrizar los mensajes.
3. Creación del proyecto de prueba
Como siempre vamos a demostrar lo que decimos y que mejor que crear un proyecto con Maven e importarlo a Eclipse. El proyecto va a utilizar Spring y Mybatis para el acceso a la base de datos, aquí tenéis un tutorial al respecto.
Definimos el pom.xml del proyecto de esta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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>com.autentia</groupId> <artifactId>custom-messagesource</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>custom-messagesource</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>3.1.0.RC1</spring.version> </properties> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.0.4</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.0.0-RC2</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>jsr250-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.0-801.jdbc4</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> </dependency> </dependencies> </project> |
Ahora vamos a crear la tabla en la base de datos que va a almacenar los literales. Simplemente va a ser una tabla que va a tener 3 columnas:
- codLiteral: almacena el código numérico del literal. INTEGER NOT NULL
- literal: almacena el texto del literal. VARCHAR (400)
- locale: almacena el idioma del literal. VARCHAR (10)
La clave primaria está compuesta por los campos codLiteral y locale, de forma que a través del mismo código vamos a poder recuperar el literal correspondiente al idioma deseado y no puede repetirse un mismo código para un mismo locale.
Cargamos una serie de datos de prueba.
Ya tenemos el entorno de desarrollo preparado.
4. Vamos al lío
Lo primero que tenemos que resolver es el acceso al literal dado un código y un locale. Para ello vamos a crear el siguiente test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.autentia.dao; import javax.annotation.Resource; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import com.autentia.model.Literal; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:/application-context-test.xml" }) @Transactional public class LiteralMapperTest { @Resource LiteralMapper literalMapper; @Test public void testGetLiteralByCodAndLocale() { Literal literal = literalMapper.getLiteralByCodAndLocale(1, "es"); Assert.assertEquals("Texto 1", literal.getLiteral()); } } |
Para la implementación del acceso a la base de datos utilizamos Mybatis, para lo cual definimos la interfaz LiteralMapper de esta forma:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.autentia.dao; import org.apache.ibatis.annotations.Param; import org.mybatis.spring.annotation.Mapper; import com.autentia.model.Literal; @Mapper public interface LiteralMapper { public Literal getLiteralByCodAndLocale(@Param("codLiteral") Integer codLiteral, @Param("locale") String locale); } |
La implementación del método definido se hace a través de un mapper en el fichero LiteralMapper.xml de esta forma:
1 2 3 4 5 6 7 |
<mapper namespace="com.autentia.dao.LiteralMapper"> <select id="getLiteralByCodAndLocale" resultType="com.autentia.model.Literal"> select * from literales where codliteral = #{codLiteral} and locale = #{locale} </select> </mapper> |
La definición del fichero application-context-text.xml define la configuración de MyBatis y la definición del datasource de prueba.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd" default-autowire="byName"> <context:annotation-config></context:annotation-config> <context:component-scan base-package="com.autentia"></context:component-scan> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <bean class="org.mybatis.spring.annotation.MapperScannerPostProcessor"> <property name="basePackage" value="com.autentia.dao"></property> </bean> <tx:annotation-driven></tx:annotation-driven> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="mapperLocations" value="classpath:/mappers/**/*.xml"></property> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.postgresql.Driver"></property> <property name="url" value="jdbc:postgresql:tutoriales"></property> <property name="username" value="postgres"></property> <property name="password" value="autentia"></property> </bean> <bean id="log4jInitializer" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.springframework.util.Log4jConfigurer.initLogging"></property> <property name="arguments"> <list> <value>classpath:log4j.properties</value> </list> </property> </bean> </beans> |
Ejecutando este test tenemos que ver que pasa perfectamente y que recupera el literal indicado. De tal forma que ya tenemos un problema resuelto.
Ahora a por el siguiente. Vamos a implementar la clase que se encargue de internacionalizar los literales a través de Spring. Esta clase tiene que extender de AbstractMessageSource e implementar el método resolveCode. Pero antes vamos al test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package com.autentia; import java.util.Locale; import javax.annotation.Resource; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.context.MessageSource; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:/application-context-test.xml" }) public class DatabaseMessageSourceTest { @Resource MessageSource messageSource; @Test public void testResolveCode() { Assert.assertEquals("Texto 1", messageSource.getMessage("1", null, new Locale("es"))); Assert.assertEquals("Texto 1 Inglés", messageSource.getMessage("1", null, new Locale("en"))); Assert.assertEquals("Texto prueba", messageSource.getMessage("3", new String[]{"prueba"}, new Locale("es"))); } } |
Como veis este código no difiere de la forma habitual de internacionalización de Spring. Tenemos tres tests: el primero de ellos nos devuelve el texto en castellano, el segundo el mismo texto en inglés y el tercero nos devuelve un texto parametrizado.
Ahora creamos la clase DatabaseMessageSource que como ya hemos dicho tiene que extender de AbstractMessageSource e implementar el método resolveCode, para nuestro caso el código quedaría de la siguiente forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.autentia; import java.text.MessageFormat; import java.util.Locale; import javax.annotation.Resource; import org.springframework.context.support.AbstractMessageSource; import org.springframework.stereotype.Service; import com.autentia.dao.LiteralMapper; import com.autentia.model.Literal; @Service public class DatabaseMessageSource extends AbstractMessageSource{ @Resource LiteralMapper literalMapper; @Override protected MessageFormat resolveCode(String code, Locale locale) { Literal literal = literalMapper.getLiteralByCodAndLocale(Integer.parseInt(code), locale.getLanguage()); return new MessageFormat(literal.getLiteral()); } } |
Simplemente accede a la base de datos con el código y el locale proporcionado para devolver el texto en un objeto MessageFormat gestionado por Spring. Ahora para que el test funcione tenemos que definir el bean «messageSource» indicando que va a ser implementado por la clase DatabaseMessageSource dentro del fichero application-context-test.xml.
1 |
<bean id="messageSource" class="com.autentia.DatabaseMessageSource"></bean> |
Ahora ejecutamos el test y tenemos que ver que pasa perfectamente. Dejo como deberes ver como funciona también en aplicaciones web utilizando la etiqueta <spring:message code=»code»/>
5. Conclusiones
Como veis prácticamente todo ya está resuelto con Spring. Nuestro primer impulso podría ser crear nuestra propia clase que gestionará estos mensajes, pero eso supondría trabajar de una forma no estándar y acoplarnos completamente a esa solución, perdiendo las ventajas de trabajar con Spring.
Cualquier duda o sugerencia en la zona de comentarios.
Saludos.