SSE con spring integration

Hace un tiempo fui a una charla sobre Spring integration en las instalaciones del Google Campus. La charla fue impartida por @ilopmar y la verdad es que me gustó mucho y me pareció que podía cambiar mi ejemplo de generación de palabras aleatorias para alimentar un cliente por SSE usando Spring integration y así poniéndolo en práctica y aclararme los conceptos.

Por ello vamos a comenzar añadiendo la dependencia de spring integration al pom.

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-integration</artifactId>
		</dependency>

A continuación comenzaremos con nuestros cambios en el código.

El primer paso va a ser definir cómo va a ser el flujo de mensajes en la aplicación.

Para soportar integration en nuestra aplicación de spring boot vamos a añadir la anotación @IntegrationComponentScan a nuestra clase de inicio de la aplicación

@SpringBootApplication
@EnableAsync
@IntegrationComponentScan
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class SseApplication {
...

Vamos a definir nuestra fuente de mensajes gracias a la clase MethodInvokingMessageSource que nos permite crear un mensaje con el payload que devuelva el método x de un objeto o, en nuestro caso, tenemos el método randomWord de la clase SpeakerPublisher del ejemplo:

@Bean
	public MessageSource<?> randomStringMessageSource(SpeakerPublisher publisher){
		MethodInvokingMessageSource source = new MethodInvokingMessageSource();
		source.setObject(publisher);
		source.setMethodName("randomWord");
		return source;
		
	}

Como el método no debe tener argumentos, vamos a modificar el método para hacerlo público

	public  String randomWord() {
	    Random random = new Random();
	    int length = 6;;
		StringBuilder word = new StringBuilder( );
	    for (int i = 0; i < length; i++) {
	        word.append((char)('a' + random.nextInt(26)));
	    }

	    return word.toString();
	}

El siguiente paso es definir un bean del tipo DirectChannel que supondrá nuestro canal de entrada

@Bean
	DirectChannel inputChannel(){
		return new DirectChannel();
	}

y otro para definir el canal de salida

@Bean 
	DirectChannel outputChannel(){
		return new DirectChannel();
	}

definimos un flujo

@Bean 
	IntegrationFlow myFlow(SpeakerPublisher publisher){
		return IntegrationFlows.from(this.randomStringMessageSource(publisher),c -> c.poller(Pollers.fixedDelay(5000)))
				.channel(inputChannel())
				.channel(outputChannel())
				.get();
	}

Podemos ver en la definición del flujo los siguientes elementos:

  • Una fuente de mensajes: nuestro bean randomStringMessageSource.
  • Un poller (servicio que realiza la extracción de alguna información cada cierto periodo de tiempo) que va a leer de nuestra fuente de datos cada 5 segundos.
  • El canal de entrada de datos en el que se van a colocar los mensajes leídos
  • El canal de salida en el que se va a mandar el resultado del flujo

Podríamos aplicar transformaciones al mensaje desde que entra en el canal de entrada hasta que llega al canal de salida, como por ejemplo pasar el mensaje a mayúsculas.

@Bean 
	IntegrationFlow myFlow(SpeakerPublisher publisher){
		return IntegrationFlows.from(this.randomStringMessageSource(publisher),c -> c.poller(Pollers.fixedDelay(5000)))
				.transform(p -> p.toString().toUpperCase())
				.channel(inputChannel())
				.channel(outputChannel())
				.get();
	}

Con esta base se pueden realizar muchos flujos de integración para leer de alguna fuente externa, transformar y escribir en algún repositorio u otro canal de comunicación.

El código lo podéis encontrar aquí

Publicado en boot, integration, sse | Etiquetado , , | Deja un comentario

Recomendadores en Mahout

¿Qué es un Recomendador?

Es la técnica usada para proporcionar al usuario recomendaciones sobre una búsqueda basada en la información que el propio usuario nos ha proporcionado previamente, como puede ser el histórico de compras, clics y puntuaciones (rating). También se conoce en otras librerías como collaborative filtering.

Vamos a ver la lógica que rodea a la construcción de un motor de recomendación usando uno de los frameworks del ecosistema de machine learning de Java, Mahout.

Tipos de recomendación

Normalmente las recomendaciones se pueden orientar en dos modos diversos, por usuario o por item.

Imaginemos que queremos comprar un cd de música a nuestra pareja. Cuando llegas a la tienda y pides al empleado que te recomiende un grupo, las dos aproximaciones serían.

Por item: ¿que suele escuchar ella?. Pues creo que le he visto escuchar alguna que otra vez a david bowie.

Por usuario: ¿que le gusta?, pues no lo se, pero sus amigos suelen llevar camisetas de metallica.

Los componentes en los que se basa la recomendación son:

  • DataModel
  • UserSimilarity
  • ItemSimilarity
  • UserNeighborhood
  • Recommender
  • Evaluator

DataModel

Es el modelo de datos a usar. Suele usarse un modelo de datos plano, separado por espacios o comas (estilo cvs) para simplificar su análisis. Esta parte, pese a que parezca lo contrario, es la más difícil, ya que depende del conocimiento del negocio, intuición etc. No es para nada sencillo obtener el juego de datos que mejores resultados obtiene para un motor de recomendación.

Los datos de entrada más simples a un recomendador se basan en preferencias, es decir, la definición de a quién le gusta algo y cuanto le gusta, por lo que la entrada estará compuesta de un id de usuario, un id de item y el valor de la preferencia.

UserSimilarity

Un algoritmo de definición de una recomendación podría ser:

for every item i that u has no preference for yet
    for every other user v that has a preference for i

     compute a similarity s between u and v

incorporate v's preference for i, weighted by s, into a running average 
return the top items, ranked by weighted average

Este algoritmo es válido, pero se haría muy lento evaluar todos los ítems, por lo cual lo que se suele hacer es establecer similitudes entre los usuarios y solamente evaluar las preferencias de los usuarios que son similares, es decir

for every other user w
compute a similarity s between u and w
retain the top users, ranked by similarity, as a neighborhood n
for every item i that some user in n has a preference for, but that u has no preference for yet
for every other user v in n that has a preference for i
compute a similarity s between u and v
incorporate v's preference for i, weighted by s, into a running average

Por ello, en el caso de que el recomendador se base en el usuario y no en el item, se hace necesario para optimizar la recomendación, la definición de unas pautas a través de las cuales se definen similitudes entre usuarios, para poder recomendar en función de las preferencias de los usuarios más símiles al usuario.

Hay muchos algoritmos de similitud, los que usa mahout son los siguientes:

  • Pearson correlation–based similarity
  • Euclidean distance
  • cosine measure similarity
  • Spearman correlation
  • Tanimoto coefficient
  • log-likelihood

Pearson correlation–based similarity:

Es una correlación básica. Su resultado se mueve de -1 a 1 y viene a indicar cómo dos números se mueven de la misma manera a través de una serie. Si un número evoluciona proporcionalmente muy parecido al otro su valor tiende al 1, si son dos evoluciones prácticamente distintas entonces tiende al 0.
El principal problema que tiene este algoritmo es que no tiene en cuenta el número de ítems en los que coinciden el par de usuarios evaluados, tan solo las puntuaciones de los ítems, aunque es de intuir que si ambos han puntuado muchos ítems  comunes seguramente tengan muchas más cosas en común que con otro usuario con el que solo ha puntuado pocos ítems de los que también has puntuado aunque no se hayan puntuado de manera similar.
Para ello se introduce una variante llamada “weighting”, mediante la cual se favorecen las puntuaciones de los ítems comunes.

Euclidean distance:

Este algoritmo se basa en la distancia entre usuarios.

Imaginemos que los usuarios son puntos en el espacio de tantas dimensiones como ítems hay. La similitud se mide como la distancia euclidiana entre ambos usuarios.
La puntuación del algoritmos se mide como 1 / (1+d), por lo que cuando son iguales el valor es 1 y baja a medida que la distancia aumenta( menos similar).

Cosine measure similarity:

Si volvemos a imaginar a los usuarios como puntos en un espacio de n dimensiones (tantas como ítems), este algoritmo lo que mide es el coseno del ángulo que forman si se une cada uno con el origen o punto (0,0,…,0), basándose en que si su racing es similar estarán cerca uno del otro por lo que el ángulo de ambas líneas será pequeño.
Con este sistema, la similitud del algoritmo va de -1 (muy diferentes) a 1(muy similares)

Si vamos a buscar el algoritmo en Mahout veremos que no existe. Esto es por que viene recogido bajo el nombre de PearsonCorrelation- Similarity!. Evidentemente no son la misma cosa, pero de cara a los resultados para este tipo de problemas donde se parte de un centro para medir, ambos van a proporcionarte el mismo resultado.

Spearman correlation:

Es una variedad el la correlación de Pearson en la que el item con menos rating se da el valor 1, el siguiente 2 y así… y posteriormente aplica el algoritmo de Pearson. El problema que tiene es la pérdida de información sobre las preferencias reales del usuario, además es bastante lento, por lo que su uso es más bien académico. Para poder usarlo en machine learning, Mahout introduce el caché en el proceso mediante un wrapper llamado CachingUserSimilarity, por lo que aumenta el performance a cambio de consumir mucha más memoria.

Tanimoto coefficient:

Este algoritmo ignora las preferencias del usuario por cada item y solo da importancia a si lo ha puntuado o no. El coeficiente Tanimoto es el número de ítems en los que dos usuarios han mostrado preferencias entre el total de ítems. Si dos usuarios son iguales tiene un valor de 1, y si son totalmente distintos tiene un valor de 0.

captura-de-pantalla-2016-09-28-a-las-22-20-45

Log-likelihood:

Este algoritmo es parecido al de Tanimoto aunque un poco más complicado de visualizar. Viene a medir cómo de improbable es que 2 usuarios coincidan en un cierto número de ítems por simple casualidad y no por que sean similares. Más cercano a 1 indica que hay más similitud y más cercano a 0 menos similitud

ItemSimilarity

Es el índice de similitud entre dos ítems, es el equivalente al UserSimilarity pero usado en los casos en el que la recomendación se base en el item y no en el usuario.

El algoritmo de recomendación sería algo así:

or every item i that u has no preference for yet for every item j that u has a preference for
compute a similarity s between i and j
add u's preference for j, weighted by s, to a running average return the top items, ranked by weighted average

Se pueden usar los mismos algoritmos de similitud y para la recomendación se tendría que usar la clase GenericItemBasedRecommender.

UserNeighborhood

Viene a ser el rango de similitud que se va a tener en cuenta a la hora de generar las recomendaciones, de tal manera que los usuarios que caigan en dicho rango son usados para definir las recomendaciones.

Hay muchos algoritmos, pero aquí vamos a comentar 2 de ellos:

Fixed-size neighborhoods:

Se mide la distancia lineal de un usuario a otro basándose en las coincidencias de preferencia (A más distancia menos parecido). Al algoritmo se le indica el número máximo de usuarios a usar para su composición.

captura-de-pantalla-2016-09-28-a-las-8-26-22

Threshold-based neighborhood:

Para este caso lo que se mide es un área y se cogen todos aquellos usuarios que estén en ese área, así no se usan los x más similares, sino los que sean más similares que cierto umbral de similitud.

captura-de-pantalla-2016-09-28-a-las-8-35-12

Recommender

Es el propio motor de recomendación, que usará los componentes anteriores para construirse.

Existe varios motores de recomendación que manejan los algoritmos antes descritos, pero en este caso vamos a ver los siguientes:

  • GenericUserBasedRecommender
  • Singular value decomposition–based recommenders

GenericUserBasedRecommender:

Es el recomendador que hemos visto, el cual tiene en cuenta similitudes, rangos de vecindad para definir los usuarios o ítems a usar para la recomendación, y el recomendador tal cual.

Singular value decomposition–based recommenders:

SVD es una técnica conocida en el álgebra lineal con bastante complejidad y para poder explicar su funcionamiento interno habría que tener conocimientos avanzados de matrices. Pero si que podemos es intuir el funcionamiento final del mismo.
Imaginemos que a una amiga le gustan Pearl jam, Nirvana, Stone temple pilots… se podría resumir que le gusta el grunge y que le gustará más un disco de Soundgarden que uno de Paulina Rubio. Esto es lo que viene a hacer el algoritmo, reduce los numerosos ítems en grupos de los mismos agrupados por características, en este ejemplo el género, que aunque menos preciso hace la computación mucho más rápida.

Para la agrupación por característica el algoritmo usa lo que llama Factorizer, que viene a ser como la similitud y vecindad del recomendador genérico.

Evaluator

El último paso es evaluar cómo de bueno es un recomendador. Para ello hay que entender primero qué significa un buen recomendador para el caso concreto. En general un buen recomendador es aquel en el que el usuario siente alegría y demuestra su preferencia por cada item que le muestras como recomendación, pero esto es prácticamente imposible ya que ni el usuario mismo sería capaz de realizarlo.
Lo que suele realizarse es “entrenar” y crear el recomendador en función de una serie de datos ya conocidos, y luego evaluarlo con datos que no ha usado para entrenarse, pero que sabemos su resultado, para evaluar su tasa de éxito. En este contexto el 0 sería el recomendador perfecto y a más puntuación más fallos tiene. Los evaluadores que nos proporciona Mahout son:

  • AverageAbsoluteDifferenceRecommenderEvaluator
  • RMSRecommenderEvaluator
  • RecommenderIRStatsEvaluator

AverageAbsoluteDifferenceRecommenderEvaluator:

Se basa en la media de las diferencias de preferencias

RMSRecommenderEvaluator

El principio es el mismo que el evaluador anterior, pero en lugar de hacer la media de las diferencias de cada preferencia, usa la raíz cuadrada de la suma de los cuadrados de las diferencias. De esta manera lo que se consigue es penalizar más las diferencias.

en resumen:
captura-de-pantalla-2016-09-29-a-las-8-41-45

RecommenderIRStatsEvaluator

Hay veces que el orden de recomendación exacto no es necesario y nos vale tan solo mostrar un conjunto de buenas recomendaciones. Para esto entra en juego lo que se conoce como precision y recall.

precision es la parte de los x mejores resultados que tienen mayor relevancia, para una definición de relevancia dada

recall es la parte de todas las recomendaciones relevantes que están incluidas en los x mejores resultados

captura-de-pantalla-2016-10-06-a-las-8-48-30

 

Si extrapolamos estas definiciones a los recomendadores, es sencillo ver que precision sería la porción de las primeras recomendaciones que son buenas recomendaciones y recall la porción de las recomendaciones buenas que aparecen entre las primeras.

Con el evaluador RecommenderIRStatsEvaluator podemos obtener estos datos y poder evaluarlos para deducir cómo de bueno es nuestro algoritmo. Imaginemos que para una porción de 2 recomendaciones obtenemos los siguientes valores:

  • precision: 0.75
  • recall: 1.0

Esto vendría a decirnos que casi 3/4 partes (2 sobre 0.75 de desviación) de las recomendaciones son buenas y todas las buenas entran en las recomendadas (2 sobre 1)

Ejemplos de código

Pongamos un conjunto de datos donde el primer dato es el id de usuario, el segundo el id de item y el tercero es el rating que le ha dado el usuario


1,101,5.0

1,102,3.0

1,103,2.5

2,101,2.0

2,102,2.5

2,103,5.0

2,104,2.0

3,101,2.5

3,104,4.0

3,105,4.5

3,107,5.0

4,101,5.0

4,103,3.0

4,104,4.5

4,106,4.0

5,101,4.0

5,102,3.0

5,103,2.0

5,104,4.0

5,105,3.5

5,106,4.0

Lo primero es leer el fichero

DataModel model = new FileDataModel (new File(SimpleRecommender.class.getResource("/examples/recommenders/basicitems.csv").toURI()));

A continuación es usar un índice de similitud

UserSimilarity similarity = new PearsonCorrelationSimilarity(model);

en este caso hemos usado el Pearson correlation.

El siguiente paso, tal y como hemos definido, es usar un criterio de cercanía en similitud

UserNeighborhood neighborhood = new ThresholdUserNeighborhood(0.7, similarity, model);

en este caso hemos usado el criterio de umbral.

Creamos el recomendador

 UserBasedRecommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);

y pedimos recomendación de 2 elementos para el usuario 1

 List<RecommendedItem> recommendations = recommender.recommend(1, 2);
			
        for (RecommendedItem recommendation : recommendations) {
           System.out.println(recommendation);
        }

y observamos que la salida del recomendador es:

Recommendations are: [RecommendedItem[item:104, value:4.2543793], RecommendedItem[item:106, value:4.0]]

Vemos que con mahout es bastante sencillo realizar un recomendador para un juego de datos no muy extenso. Para casos de cantidades de datos muy grandes se introduce bigdata con el soporte que da para spark o hadoop.
En un caso real habría que experimentar con los distintos algoritmos y similitudes para comprobar cual es el que mejor funciona.

Lo siguiente va a ser evaluar cómo de bueno es este recomendador, para ello vamos a usar el AverageAbsoluteDifferenceRecommenderEvaluator:

DataModel model = new FileDataModel (new File(SimpleRecommender.class.getResource("/examples/recommenders/basicitems.csv").toURI()));
	
		
		RecommenderBuilder builder = new RecommenderBuilder() { @Override
			public Recommender buildRecommender(DataModel model)
			      throws TasteException {
		//Create UserRecomender
			UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
			UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);
			UserBasedRecommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
			return recommender;
			}
		};
			
        RecommenderEvaluator evaluator =
        		new AverageAbsoluteDifferenceRecommenderEvaluator ();
        double score = evaluator.evaluate(
        	    builder, null, model, 0.7, 1.0);
        	System.out.println(score);

Y vemos que en este caso el resultado es Nan, es decir, el juego de datos es muy pequeño para obtener resultados con esa configuración.

Si cambiamos el criterio de similitud por LogLikelihoodSimilarity en este caso nos da un resultado de 1. Esto es bueno o malo?, bien la interpretación de este dato es que hay una desviación de 1 con respecto a 5 puntos posibles, por lo que hay un 20% de error, que no está mal.

Veamos un ejemplo con SVD

RandomUtils.useTestSeed();
		DataModel model = new FileDataModel (new File(SVDSimpleRecommender.class.getResource("/examples/recommenders/basicitems.csv").toURI()));
		
		RecommenderBuilder builder = new RecommenderBuilder() { 
			@Override
			public Recommender buildRecommender(DataModel model)
			      throws TasteException {
				Recommender recommender = new SVDRecommender(model, new ALSWRFactorizer(model, 10, 0.05, 10));
				return recommender;
			}
		};
        
        RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator ();
        double score = evaluator.evaluate(builder, null, model, 0.7, 1.0);
        System.out.println(score);

y para nuestro ejemplo da un resultado de 2.304866830507914, lo cual supone un error bastante mayor.

Esto han sido solo un par de ejemplos simples para entender la lógica que hay detrás de un recomendador y su manera de enfocarlo en Mahout. Lo difícil es obtener un juego de datos amplio y limpio, que esté bien estructurado, entender el significado de los mismos, el negocio detrás de él para poder entender qué evaluar y, a partir de ahí, probar algoritmos, configuraciones y muchas, muchas pruebas hasta llegar a resultados buenos.

Publicado en machine learning, mahout | Etiquetado , , | Deja un comentario

Single Side Event con Spring boot

Recientemente me he encontrado con la necesidad de tener que refrescar un listado ante cierto tipo de evento que sucede en una tercera parte. Para estos casos, y aprovechando que usaba la versión 1.4.1 de spring boot, decidí usar Single Side Event.

¿Qué es Single side event?

Basicamente es el nombre que se da al hecho que que el servidor comunique la existencia de un evento al cliente.

Es importante recalcar que, a diferencia con los websockets, la comunicación no es dual, sino que va solo del servidor al cliente.

¿Diferencias con Websockets?

Básicamente la diferencia es la unitariedad de la comunicación con el cliente. Mientras en websockets la comunicación es duplex, en sse es solo de servidor al cliente. Otra diferencia es que sse usa comunicación Http y no por sockets, por lo que es más sencilla.
Por último indicar que todo lo que puede hacer sse lo puede hacer websockets.

Ventajas de SSE sobre Websockets:

  • Transportado por Http en lugar de por un protocolo ad hoc.
  • Viene de serie la reconexión y el eventid en el cliente
  • Protocolo más simple

Ventajas de Websockets sobre SSE :

  • Comunicación bidirecional en Real time
  • Soportado de manera nativa por más navegadores

¿Cómo implementarlo?

Con Spring es muy fácil de implementar. Vamos a hacer un ejemplo donde una web conecta con el servicio de eventos para escuchar palabras  y las va poniendo en la web.

Los componentes de este ejemplo son los siguientes:

  • Un controlador rest que registra al nuevo cliente
  • Un listener de eventos que espera que le manden una palabra para enviarla a su amigo
  • Un emisor de palabras
  • Un cliente que escucha y lo muestra en la página

Cliente

La parte del cliente la realizaremos en javascript. No hay que instalar ninguna librería adicional, ya viene el soporte con el propio navegador, aunque hay que tener en cuenta que no todos los navegadores lo soportan.
Las versiones desde las cuales viene soportada esta funcionalidad vienen descritas en esta tabla:
captura-de-pantalla-2016-10-12-a-las-20-48-35
Veamos el código:


var source = new EventSource("/listen");
source.addEventListener('open', function (e) {
    console.log('connected');
});

source.addEventListener('message', function (e) {
    console.log(e.data);
    //var message = JSON.parse(e.data);
    var message = e.data;
  
    $('#chatlog').html( $('#chatlog').html() + '

'+ message + '

');
}, false);

source.addEventListener('error', function (e) {
    if (e.readyState == EventSource.CLOSED) {
        connected = false;
        connect();
    }
}, false);

Vemos que tan solo hay que poner la url al controlador que nos registra con un objeto EventSource y escribir la función que se ejecutará cuando se reciba un mensaje

Registro

Veamos a nuestro controlador que registrará al cliente:


@RestController
@Data
public class RegisterController {
	
	private final int CLIENT_ID = 1;
	private final SpeechListener speechListener;
	

	@RequestMapping("/listen")
	SseEmitter listen() throws IOException {
		final SseEmitter sseEmitter = new SseEmitter();
		
		sseEmitter.send("hello, start to speak");
		speechListener.addSseEmitters(CLIENT_ID, sseEmitter);
		
		sseEmitter.onCompletion(() -> speechListener.remove(CLIENT_ID));
		    
		return sseEmitter;
		
	}
}

Vemos que para el ejemplo solo vamos a registrar un único emisor o Emitter con el id 1.
Con tan solo devolver dicho objeto en la respuesta el cliente se quedará escuchando al servidor

Emisor

Vamos a emitir un evento cada 5 segundos que generará una palabra aleatoria de 6 caracteres.
Veamos el código

@Service
@Data
public class SpeakerPublisher {

	private static final int CLIENT_ID = 1;
	private final ApplicationEventPublisher eventPublisher;


	@Scheduled(fixedDelay=5000)
	public void speak() {
		
		eventPublisher.publishEvent(new SpeechEvent(this, randomWord(6), CLIENT_ID));
	}
	
	public  String randomWord(int length) {
	    Random random = new Random();
	    StringBuilder word = new StringBuilder(length);
	    for (int i = 0; i < length; i++) {
	        word.append((char)('a' + random.nextInt(26)));
	    }

	    return word.toString();
	}
}

En este caso hemos usado la parte de programación de tareas de Spring y la gestión de eventos de Spring, pero se entiende fácil que cada 5 segundos se ejecuta el método speak que publica un evento con la palabra generada como mensaje.

Listener

Veamos el listener que ejecuta nuestro evento y que enviará la palabra al cliente

@Component
public class SpeechListener {
	
	private static Logger logger = org.slf4j.LoggerFactory.getLogger(SpeechListener.class);
	
	/**
     * The list of the objects of SseEmitter.
     * The key of the map stands for submissionId.
     * The value of the map is the corresponding SseEmitter object.
     */
    private static Map<Long, SseEmitter> sseEmitters = new Hashtable<Long, SseEmitter>();

	@EventListener
	public void submissionEventHandler(SpeechEvent event) {
		long listenerId = event.getListenerId();
		String message = event.getMessage();
		SseEmitter sseEmitter = sseEmitters.get(listenerId);

		if (sseEmitter == null) {
			logger.warn(String.format("CANNOT get the SseEmitter for listener #%d.", listenerId));
			return;
		}
		 try {
			<strong>sseEmitter.send(message);</strong>
		 } catch (IOException e) {
			 sseEmitter.complete();
             sseEmitters.remove(listenerId);
             logger.warn(e.getMessage());
         }
	}
...

Hemos subrayado en negrita la parte de código que realiza el envío. Así de fácil, solo hay que invocar el método send del Emitter vinculado al cliente y se envía el contenido que se le pasa en el mismo.

El código del ejemplo lo podéis descargar de aquí

Publicado en boot, java | Etiquetado , | Deja un comentario

Machine Learning

Estoy comenzando a introducirme al mundo del machine learning, un mundo que me ha atraído de siempre ya que todo lo relacionado con la inteligencia artificial ha sido fascinante para mí desde la infancia.

Como siempre, para poder aclararme y consolidar lo aprendido he decidido escribir sobre ello.

¿Qué es machine learning?

Machine learning viene a ser como una parte de la ciencia (no es exclusivo del software) que tiene que ver con la programación de sistemas de tal manera que sean capaces de aprender y adaptarse de la propia experiencia. Para ello el aprendizaje se basa en tomar las decisiones según la entrada de datos en función de un conjunto de datos proporcionados previamente los cuales nos dan información de la decisión correcta para los mismos.

Como esta toma de decisiones es bastante compleja, el uso de algoritmos matemáticos complejos se hace necesario. Por ello es positivo poseer  conocimientos de cálculos estadísticos, teoría de las probabilidades, lógica, combinatoria …

A nivel de software, hay muchas utilidades, pero su uso más extendido, y que será el objeto inicial del estudio, ha sido en las siguientes funcionalidad:

  • recomendación
  • clasificación
  • clustering

Como nota para próximas entradas, conviene conocer los siguientes términos:

Aprendizaje supervisado: Se usa dicho término cuando se dispone de un conjunto de datas cuyo resultado es conocido para poder entrenar el sistema. Usos típicos:

  • clasificación de e-mails como spam,
  • etiquetado de webpages basándose en su contenido
  • reconocimiento vocal.

Ejemplos algoritmos supervisados: neural networks, Support Vector Machines (SVMs), y clasificadores Naive Bayes.

Aprendizaje no supervisado: Se usa dicho término cuando no se dispone de un conjunto de datos conocidos para entrenar al sistema en su toma de decisiones. Suele ser muy útil este tipo de aprendizaje para sacar patrones y tendencias en los datos.

Ejemplos algoritmos supervisados: k-means, self-organizing maps, y clustering jerárquicos.

Recomendadores

Es la técnica usada para proporcionar al usuario recomendaciones sobre una búsqueda basada en la información que el propio usuario nos ha proporcionado previamente, como puede ser el histórico de compras, clics y puntuaciones (racing). Ejemplo de páginas que lo usan:

  • Amazon usa esta técnica para recomendarte otros productos que puede que te interesen en función de lo que otros usuarios que compraron el producto han comprado también.
  • Facebook usa esta técnica para identificar y recomendar “personas que podrías conocer”.
  • LinkedIn usa esta técnica para identificar y recomendar “personas que podrías conocer”.

Clasificación

Esta técnica suele usarse para determinar cómo debe clasificarse los datos proporcionados dentro de un conjunto de categorías existentes. Esta técnica es supervisada.

  • Servicios de email como Google o Yahoo usan esta técnica para clasificar si un email entrante es spam o no.
  • Spotify usa clasificación para crear radios relacionadas con un artista.

Clustering

Clustering se usa para crear agrupaciones de datos según características que tengan en común. Es una técnica no supervisada.

  • Linkedinusa esta técnica para agrupar un comentario en grupos como empleo, promoción.
  • Newsgroups usa esta técnica para agrupar los artículos en función de topics que estén relacionados.

Tras esta introducción intentaremos ir escribiendo sobre cada una de las categorías poniendo ejemplos.

Publicado en big data, machine learning | Etiquetado | Deja un comentario

redirect de spring web flow con zuul

Uno de los problemas que he encontrado últimamente usando Spring cloud oss ha sido el de la integración de Spring web flow (swf) con Zuul.

Si habéis usado swf sabréis que usa el patrón post-redirect para cada una de las llamadas que recibe para manejar así los reloads de página de una manera limpia. El problema viene cuando estás usando una infraestructura como Spring cloud bajo netflix oss. En esta arquitectura tenemos algo parecido a lo siguiente:

zuul problem.001

Como podemos observar tenemos n servicios UI que están sirviendo las peticiones las cuales llegan balanceadas gracias a zuul. Imaginemos que dichos servicios están en los siguientes puntos del cloud:

  • service1: host1:8085
  • service2: host2:8085

Cuando realizamos una llamada a nuestro servicio en http://mydomain.com/myuri dicha petición será escuchada por nginx el cual la redirigirá  a Zuul para que la sirva. Zuul a su vez lo mandará a uno de nuestros servicios gracias al registro de los mismos en Eureka. Pongamos que es service1 quién la sirve. Al hacer post-redirect, la respuesta va a llevar un header 301 y un header “location” indicando la nueva url a la cual redirigirse. Lo esperado sería que dicho location fuera http://mydomain.com/myuri?execution=e1s1, la cual sería perfecta por que la nueva llamada volvería a pasar por toda la infraestructura y la serviría alguno de nuestros servicios sin problemas. Pero no, la url que nos proporciona swf en estos momentos (usando la versión starter 1.3.5  en la versión cloud proporcionada por la Brixton.M4) es http://service1:8085/myuri?execution=e1s1

zuul problem.002

El problema que se crea es el siguiente: Nuestro sistema tiene capada dicha url por seguridad, por lo que acabamos obteniendo una respuesta 500.

Zuul, al menos en esta versión, no maneja bien esta casuística ( al parecer tienen un ticket abierto gracias al cuál esperemos que integren la solución en breve), por lo que la solución pasa por crear un filtro en zuul que solucione este problema.

Si no conocéis los filtros de Zuul, os resumiré que Zuul nos permite facilmente definir filtros para manejar una petición tanto antes, como durante y después de servirla. En su integración con spring boot implementar un filtro zuul es muy sencillo. Solo hemos de seguir los siguientes pasos:

  1. crear una clase que extienda com.netflix.zuul.ZuulFilter
  2. definir en qué punto de la cadena de filtros que se aplicarán va a alojarse dicho filtro sobreescribiendo el método filterOrder
  3. indicar si el filtro se aplica antes, durante o después del enrutado de la petición sobreescribiendo el método filterType especificando uno de los valores para dichos momentos pre, route, post

La implementación que realizaremos será la siguiente: Cambiaremos el header location por la llamada al servicio genérico manteniendo toda la uri y queryparams que contiene el valor aportado por spring web flow. Con esta aproximación el filtro quedaría tal que así:


import java.net.MalformedURLException;

import java.net.URL;

import java.util.Optional;

import org.slf4j.Logger;

import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;

import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator.ProxyRouteSpec;

import org.springframework.cloud.netflix.zuul.filters.RouteLocator;

import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import org.springframework.web.util.UrlPathHelper;

import com.netflix.util.Pair;

import com.netflix.zuul.ZuulFilter;

import com.netflix.zuul.context.RequestContext;

public class LocationHeaderRewritingFilter extends ZuulFilter{

protectedstaticfinal String X_FORWARDED_FOR = "X-Forwarded-For";

private Logger log = org.slf4j.LoggerFactory.getLogger(this.getClass());

final UrlPathHelper urlPathHelper = new UrlPathHelper();


final RouteLocator routeLocator;


public LocationHeaderRewritingFilter(RouteLocator routeLocator){

this.routeLocator = routeLocator;

}


@Override

public Object run() {

RequestContext ctx = RequestContext.getCurrentContext();

ProxyRouteSpec route = ((ProxyRouteLocator)routeLocator).getMatchingRoute(urlPathHelper.getPathWithinApplication(ctx.getRequest()));


if (route == null)  return null;


try {

Pair<String, String> lh = locationHeader(ctx);

URL netUrl = new URL(lh.second());


String host = netUrl.getProtocol() +"://" + netUrl.getHost();

String proxyHost = retrieveNewLocationUrl(route);

lh.setSecond(lh.second().replace(

host,

proxyHost));


if(!netUrl.getPath().startsWith(route.getPrefix())){


lh.setSecond(lh.second().replace(

proxyHost,

proxyHost + route.getPrefix()));

}


} catch ( MalformedURLException e) {

log.error("cannot change location from url");

}


return null;

}

private String retrieveNewLocationUrl(ProxyRouteSpec route) {

return ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();

}

@Override

publicboolean shouldFilter() {

return locationHeader(RequestContext.getCurrentContext()) != null;

}

@Override

publicint filterOrder() {

return 100;

}

@Override

public String filterType() {

return"post";

}

private Pair<String, String> locationHeader(RequestContext ctx) {

Optional<Pair<String, String>> first = ctx.getZuulResponseHeaders().stream().filter( h -> "Location".equals(h.first())).findFirst();

returnfirst.orElse(null);

}

}

Espero que os sirva de utilidad

Publicado en boot, cloud, netflix oss | Etiquetado , , , | Deja un comentario

dos datasources en spring boot

Hace tiempo que no escribo ya que he estado inmerso en un nuevo proyecto en la empresa en la que trabajo actualmente donde hemos podido aplicar toda la arquitectura de spring cloud netflix oss y no he tenido tiempo para dedicarle al blog. Para no dilatarlo más en el tiempo he decidido crear algunas entradas nuevas en las que explique algunos de los problemas/dudas que hemos encontrado durante el desarrollo del mismo.

Una de las dudas que nos surgió es, ¿cómo declaro el uso de dos datasources en spring boot?

Normalmente la declaración del datasource que use el microservicio se realiza mediante la entrada en el fichero application.yml de este estilo:

spring:
  datasource:
    url: jdbc:oracle:thin:myuri
    username: username
    password: pass
    driver-class-name: driver

En el caso de ser necesario el uso de dos datasources (algo que debe ser excepcional si es un microservicio real) su declaración pasa a ser de esta forma:


datasource:
  nameddbb1:
    url: jdbc:mysql://uriddbb1
    username: usernameddbb1
    password: passbbdd1
    driver-class-name: com.mysql.jdbc.Driver
  nameddbb2:
    url: jdbc:oracle:thin:uriddbb2
    username: usernameddbb2
    password: pwdddbb2
    driver-class-name: oracle.jdbc.driver.OracleDriver

espero que haya sido útil

Publicado en boot | Etiquetado , | Deja un comentario

spring boot y twitter (sign in framework)

Introducción

Tras haber realizado el ejemplo simple de conexión y logado mediante twitter, el siguiente paso sería el de realizar la versión en la que gestionemos nosotros las credenciales, es decir, que recibamos el callback con los datos de la conexión y los del usuario que nos proporciona twitter y los almacenamos y controlamos el registro. Esto es lo que se conoce como framework “sign in”. Para más información podéis visitar el link sobre su sign in with service provider

Spring Security

En primer lugar vamos a añadir Spring Security a nuestra aplicación. Para lo cual solo hay que añadir a nuestro pom las dependencias:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-security</artifactId>
		</dependency>

Observamos que también hemos añadido la parte de seguridad de Spring para la parte social. Si en estos momentos arrancamos la aplicación, veremos que se añade al log de arranque una línea tal como esta

Using default security password: 3c2a7a66-5e57-40a5-9007-7ec195b28292

La cual indica que esa es la password que hay que usar para acceder a cualquier página de la aplicación, usando el usuario “user“. Como esto no es lo que queremos, a continuación definimos la configuración para poder indicar qué páginas van a ser seguras y cuales no. Para ello, tal como se realizan las configuraciones usando Spring boot, creamos nuestra clase de configuración, la cual será WebSecurityConfig, anotándola con @Configuration. Siguiendo las reglas que nos indica Spring Security, lo que hemos de hacer es anotar la clase con @EnableWebSecuirty y proporcionar un bean del tipo WebSecurityConfigurerAdapter donde se va a sobreescribir este mapeo de seguridad sin alterar nada más, y anotada con  @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) para indicar la sobreescritura. Aprovecharemos la propia clase de configuración para extender la clase y anotarla de ese modo. Sobreescribimos el método configure y anulamos de momento toda seguridad ya que la iremos definiendo poco a poco. El resultado es el siguiente:

 
@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

@Override
	protected void configure(HttpSecurity http) throws Exception {
              http.authorizeRequests().anyRequest().permitAll();
        }

Flujo

Vamos a partir de nuestra página home, a la que vamos a cambiar respecto al ejemplo anterior, para aprovechar el motor de layouts de thymeleaf:

Captura de pantalla 2015-10-18 a las 19.15.42

El contenido pasa a ser:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:social="http://spring.io/springsocial" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
  <body>


<div id="content" class="container" layout:fragment="content">

<div class="jumbotron">

<h1>Welcome to social login project!</h1>

</div>

</div>


  </body>
</html>

donde vemos el uso del layout decorator para decirle la plantilla sobre la cual modificamos el contenido. Dicha plantilla será la página layout.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:social="http://spring.io/springsocial" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
	<head>
		<title>Spring Social Showcase</title>
		 <link href="/webapp/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
	     <link href="/webapp/css/sticky-footer-navbar.css" th:href="@{/css/sticky-footer-navbar.css}" rel="stylesheet"/>

	</head>
	<body>


<div th:replace="userbars/userbar :: #bar">userbar</div>


		


<div id="content" layout:fragment="content">
			Content goes here
		</div>


		
		


<div th:replace="footers/footer :: #footer">footer</div>


		<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
		<script src="/js/bootstrap.min.js"></script>
	</body>
</html>

Como podemos observar tenemos 2 opciones de menú, logarnos en la aplicación o registrarnos. En primer lugar vamos a ver cómo logamos  mediante twitter.

Spring Social

El primer paso es crear la página de logado:

Captura de pantalla 2015-10-18 a las 19.36.29

para la cual hemos usado una plantilla de bootstrap llamada signin.css. Vemos aquí el contenido de la página signin.html.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:social="http://spring.io/springsocial" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <title>Social Login Example Sign in Page</title>

    <!-- Bootstrap -->
    <link href="../../webapp/css/bootstrap.min.css" th:href="@{css/bootstrap.min.css}" rel="stylesheet"/>
    <link href="../../webapp/css/font-awesome.min.css" th:href="@{css/font-awesome.min.css}" rel="stylesheet"/>
    <link href="../../webapp/css/bootstrap-social.css" th:href="@{css/bootstrap-social.css}" rel="stylesheet"/>
    <!--     <link href="css/navbar-fixed-top.css" rel="stylesheet"/> -->
    <link href="../../webapp/css/sticky-footer-navbar.css" th:href="@{css/sticky-footer-navbar.css}" rel="stylesheet"/>
    <link href="../../webapp/css/signin.css" th:href="@{css/signin.css}" rel="stylesheet"/>

  </head>
  <body>


<div id="content" class="container" layout:fragment="content">




<form class="form-signin" id="signin" th:action="@{/signin/authenticate}" method="post">
				<input type="hidden" name="_csrf" th:value="${_csrf.token}"></input>


<div class="formInfo">


<div class="error" th:if="${param.error eq 'bad_credentials'}">
						Your sign in information was incorrect. Please try again or <a th:href="@{/signup}">sign up</a>.
					</div>




<div class="error" th:if="${param.error eq 'multiple_users'}">
						Multiple local accounts are connected to the provider account. Try
						again with a different provider or with your username and
						password.</div>


				</div>




<h2 class="form-signin-heading">Please sign in</h2>


        <label for="username" class="sr-only">User Name</label>
        <input type="username" id="username" class="form-control" placeholder="User Name" name="username" required="" autofocus=""></input>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" id="inputPassword" class="form-control" placeholder="Password" name="password" required=""></input>


<div class="checkbox">
          <label>
            <input type="checkbox" value="remember-me"> Remember me</input>
          </label>
        </div>


        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      </form>






<div class="jumbotron">


Some test user/password pairs you may use are:



<ul>


<li>elvis/presley</li>




<li>michael/jackson</li>


				</ul>





					Or you can <a th:href="@{/signup}">signup</a> with a new account.
				

			<!-- TWITTER SIGNIN -->
			


<form id="tw_signin" th:action="@{/signin/twitter}" method="POST">
				<input type="hidden" name="_csrf" th:value="${_csrf.token}"></input>
				<button class="btn btn-social btn-twitter" type="submit">
					<i class="fa fa-twitter"></i> Sign in with Twitter
				</button>
			</form>


		</div>


	</div>


</body>
</html>

Vemos que podemos usar un username y password registrados en la aplicación, o usar nuestra cuenta de twitter. Spring Social funciona de la siguiente manera:

  1. en el caso de que el usuario no esté logado en la aplicación llamará a la url /signup para invitar al usuario a registrarse
  2. en el caso de que esté ya registrado llamará a la url GET /signin/{providerId}?code={verifier}

En nuestra primera iteración, al no tener registrado el usuario en la aplicación, mostraremos nuestra página de registro con los datos de usuario obtenidos de twitter para que se registre sin apenas tener que rellenar datos:
Captura de pantalla 2015-10-18 a las 20.07.44 Para este comportamiento hemos modificado nuestro SignUpController de la siguiente manera:

...
@RequestMapping(value = "/signup", method = RequestMethod.GET)
	public SignupForm showRegistrationForm(WebRequest request, Model model) {
		
		Connection<?> connection = providerSignInUtils.getConnectionFromSession(request);
		if (connection != null) {
			request.setAttribute("message", new Message(MessageType.INFO, "Your " + StringUtils.capitalize(connection.getKey().getProviderId()) + " account is not associated with a Spring Social Showcase account. If you're new, please sign up."), WebRequest.SCOPE_REQUEST);
			SignupForm signupForm = SignupForm.fromProviderUser(connection.fetchUserProfile());
			return signupForm;
		} else {
			return new SignupForm();
		}
		
	}
...

Como vemos vamos a partir de SignupForm, un objeto que va a implementar el futuro formulario de creación de cuenta en la aplicación. Dicho objeto es el siguiente:

@Data
public class SignupForm {

	@NotEmpty
	private String username;

	@Size(min = 6, message = "must be at least 6 characters")
	private String password;

	@NotEmpty
	private String firstName;

	@NotEmpty
	private String lastName;

	@NotEmpty
	private String email;

	public static SignupForm fromProviderUser(UserProfile providerUser) {
		SignupForm form = new SignupForm();
		form.setFirstName(providerUser.getFirstName());
		form.setLastName(providerUser.getLastName());
		form.setUsername(providerUser.getUsername());
		form.setPassword(UUID.randomUUID().toString());
		form.setEmail(providerUser.getEmail());
		form.setEmail("NO EMAIL");
		return form;
	}
}

Una vez rellenados todos los datos se nos creará una cuenta en la aplicación y se nos logará en la misma. Si todo va bien la aplicación nos redirigirá a nuestra home.
Captura de pantalla 2015-10-18 a las 20.10.30
Para conseguir toda esta parte hemos de crear toda la infraestructura de registro y creación de cuentas en nuestro sistema. La parte del controlador que recibe nuestra llamada POST a /signup será la siguiente:

...
@RequestMapping(value="/signup", method=RequestMethod.POST)
	public String signup(@Valid SignupForm form, BindingResult formBinding, WebRequest request) {
		if (formBinding.hasErrors()) {
			return null;
		}
		Account account = createAccount(form, formBinding);
		if (account != null) {
			SignInUtils.signin(account.getUsername());
			providerSignInUtils.doPostSignUp(account.getUsername(), request);
			return "redirect:/";
		}
		return null;
	}

private Account createAccount(SignupForm form, BindingResult formBinding) {
		try {
			Account account = new Account(form.getUsername(), form.getPassword(), form.getFirstName(), form.getLastName(), form.getEmail());
			accountRepository.create(account);
			return account;
		} catch (UsernameAlreadyInUseException e) {
			//formBinding.rejectValue("username", "user.duplicateUsername", "already in use");
			return null;
		}
	}
...

Vamos a poner el método de creación de cuentas en el propio controlador por simplificar la estructura del ejemplo. Vemos que vamos a mapear los datos del formulario a un objeto Account

import lombok.Data;

@Data
public class Account {

	private final String username;

	private final String password;

	private final String firstName;

	private final String lastName;

	private final String email;
}

Este objeto sera pasado como argumento a un objeto que implementa la interfaz AccountRepository, el cual va a realizar la persistencia en base de datos. Dicha interfaz es la siguiente:

public interface AccountsRepository {

	void create(Account account) throws UsernameAlreadyInUseException;

	Account findAccountBy(String username);
}

Para llegar a su implementación primero vamos a definir la base de datos y cómo conectarnos a ella.

conexión a base de datos hsqldb

Vamos a almacenar los usuarios en base de datos, para ello vamos a usar la base de datos embebida hsqldb. Lo que haremos será, en primer lugar, añadir las dependencias maven:

	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.hsqldb</groupId>
	    <artifactId>hsqldb</artifactId>
	    <scope>runtime</scope>
	</dependency>

A continuación, y tal y como nos indica la referencia de spring boot para base de datos embebidas, definimos un fichero en el raíz del classpath llamado schema.sql para definir las tablas que queremos crear y un fichero data.sql para la carga inicial de las mismas. En nuestro caso el contenido será el siguiente:

create table Account (  id identity,
						username varchar (50) unique,
						password varchar (50) not null,
						firstName varchar (50) not null, 
						lastName varchar (50) not null,
						email varchar (50) not null);

repositorio

A continuación definimos la clase que realizará la persistencia y búsqueda de las cuentas de usuario y que implementará la interfaz AccountRepository:

@Repository
public class JdbcAccountRepository implements AccountsRepository {

	private final JdbcTemplate jdbcTemplate;

	private final PasswordEncoder passwordEncoder;

	@Inject
	public JdbcAccountRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
		this.jdbcTemplate = jdbcTemplate;
		this.passwordEncoder = passwordEncoder;
	}

	@Transactional
	public void create(Account user) throws UsernameAlreadyInUseException {
		try {
			jdbcTemplate.update(
					"insert into Account (firstName, lastName, username, password, email) values (?, ?, ?, ?, ?)",
					user.getFirstName(), user.getLastName(), user.getUsername(),
					passwordEncoder.encode(user.getPassword()), user.getEmail());
		} catch (DuplicateKeyException e) {
			throw new UsernameAlreadyInUseException(user.getUsername());
		}
	}

	public Account findAccountBy(String username) {
		return jdbcTemplate.queryForObject("select username, firstName, lastName, email from Account where username = ?",
				new RowMapper<Account>() {
					public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
						return new Account(rs.getString("username"), null, rs.getString("firstName"), rs
								.getString("lastName"), rs.getString("email"));
					}
				}, username);
	}

}

Analizando este código, podemos ver que hemos usado el template jdbc de Spring para el acceso simple a la base de datos. El segundo paso es definir el login a la aplicación en el caso de que estés ya registrado.

Spring Security

Veamos de nuevo la página de login:

Captura de pantalla 2015-10-18 a las 19.36.29
En primer lugar definimos a grano más fino las urls securizadas:

definición de estáticos

Sobreescribimos en nuestra clase WebSecurityConfig el método:

@Override
	public void configure(WebSecurity web) throws Exception {
		web
			.ignoring()
				.antMatchers("/**/*.css", "/**/*.png", "/**/*.gif", "/**/*.jpg, /**/*.js");
	}

definición de seguridad a nivel de url,

hay que sobreescribir el método Para las urls de login definimos esta parte:

@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.formLogin()
			.loginPage("/signin")
			.loginProcessingUrl("/signin/authenticate")
			.failureUrl("/signin?param.error=bad_credentials")
...

para permitir la url de logout de la aplicación:

...
.and()
				.logout()
					.logoutUrl("/signout")
					.deleteCookies("JSESSIONID")
					.invalidateHttpSession(true)
					.permitAll()
...

y las urls permitidas de libre acceso

...
		.and()
				.authorizeRequests()
					.antMatchers("/admin/**", "/favicon.ico", "/resources/**", "/auth/**", "/signin/**", "/signup/**").permitAll()
					.antMatchers("/**").authenticated();
}

El siguiente paso es definirle a Spring Security el repositorio al que consultar por las credenciales de los usuarios que intentan logarse. Para ello le inyectamos a la clase WebSecurityConfig el datasource de nuestra base de datos hsql y sobreescribimos el método registerAuthentication en el que le indicamos cómo realizar la consulta:

...
@Inject
private DataSource dataSource;
@Autowired
	public void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
		auth.jdbcAuthentication()
				.dataSource(dataSource)
				.usersByUsernameQuery("select username, password, true from Account where username = ?")
				.authoritiesByUsernameQuery("select username, 'ROLE_USER' from Account where username = ?")
				.passwordEncoder(passwordEncoder());
	}
...

Con esto ya nuestra aplicación realizará el login y permitirá acceder al usuario a las urls seguras.

Login de twitter con usuario registrado

Vamos a ver el caso en el que el usuario ya existe  en la aplicación y uno accede mediante su usuario de twitter Cuando el usuario pulsa el botón de twitter, se registra y vuelve a la aplicación, gracias a la integración de Spring social con Spring security, la aplicación va a comprobar que dicho usuario ya está registrado como usuario de la aplicación, por lo que va a llamar al signin adapter definido en el sistema para realizar dicho logado a la aplicación. Para poder controlar el comportamiento vamos a sobreescribir la clase  SignInAdapter de Spring social tal y como nos dice su referencia.

public class MySignInAdapter implements SignInAdapter{
	
	private final RequestCache requestCache;

	public MySignInAdapter(RequestCache requestCache) {
		this.requestCache = requestCache;
	}
	
	@Override
	public String signIn(String localUserId, Connection<?> connection, NativeWebRequest request) {
		SignInUtils.signin(localUserId);
		return extractOriginalUrl(request);
	}

	private String extractOriginalUrl(NativeWebRequest request) {
		HttpServletRequest nativeReq = request.getNativeRequest(HttpServletRequest.class);
		HttpServletResponse nativeRes = request.getNativeResponse(HttpServletResponse.class);
		SavedRequest saved = requestCache.getRequest(nativeReq, nativeRes);
		if (saved == null) {
			return null;
		}
		requestCache.removeRequest(nativeReq, nativeRes);
		removeAutheticationAttributes(nativeReq.getSession(false));
		return saved.getRedirectUrl();
	}
		 
	private void removeAutheticationAttributes(HttpSession session) {
		if (session == null) {
			return;
		}
		session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
	}

}

Observando este código vemos que lo que hará será el realizar el signin en la aplicación gracias al SigninUtils y a continuación obtener la url original para redirigir a ella después del logado en la aplicación. El código de la clase SigninUtils es el siguiente:


public class SignInUtils {

	/**
	 * Programmatically signs in the user with the given the user ID.
	 */
	public static void signin(String userId) {
		SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(userId, null, null));	
	}
}

Vemos que tan solo se va a encargar de añadir al usuario dentro de Spring Security como usuario autenticado. Por último hay que indicarle a Spring Social que use nuestro SigninAdapter para lo cual definimos una clase de configuración de la parte social:


@Configuration
@EnableSocial
public class SocialConfig {
	
    @Bean
	public SignInAdapter signInAdapter() {
		return new MySignInAdapter(new HttpSessionRequestCache());
	}
}

Con esto hemos terminado la integración y cumplido el flujo de trabajo que queríamos tratar.

twitter

Como vemos ahora nos aparece una bienvenida en la parte superior derecha con nuestro nombre de usuario y el link para deslogarse de la aplicación. También aparece una sección “twitter” donde encontraremos algunas acciones que podemos hacer fácilmente con la api de Spring Social de Twitter:

Captura de pantalla 2015-10-18 a las 20.13.01

 profile

Veamos el controlador

@Controller
public class ProfileController {

	private final Twitter twitter;

	@Inject
	public ProfileController(Twitter twitter) {
		this.twitter = twitter;
	}
	
	@RequestMapping(value="/twitter/profile", method=RequestMethod.GET)
	public String home(Principal currentUser, Model model) {
		model.addAttribute("profile", twitter.userOperations().getUserProfile());
		return "twitter/profile";
	}
}

y vemos aquí el resultado:

Captura de pantalla 2015-10-22 a las 22.56.06

Seguidores y siguiendos

Veamos el controlador

@Controller
public class FriendsController {

	private final Twitter twitter;
	
	@Inject
	public FriendsController(Twitter twitter) {
		this.twitter = twitter;
	}
	
	@RequestMapping(value="/twitter/friends", method=RequestMethod.GET)
	public String friends(Model model) {
		model.addAttribute("twitterProfile", twitter.userOperations().getUserProfile());
		model.addAttribute("friends", twitter.friendOperations().getFriends());
		return "twitter/friends";
	}

	@RequestMapping(value="/twitter/followers", method=RequestMethod.GET)
	public String followers(Model model) {
		model.addAttribute("twitterProfile", twitter.userOperations().getUserProfile());
		model.addAttribute("friends", twitter.friendOperations().getFollowers());
		return "twitter/friends";
	}
}

y vemos aquí el resultado:

Captura de pantalla 2015-10-22 a las 22.57.08

Trends

Veamos el controlador

@Controller
public class TrendsController {

	// Yahoo Where On Earth ID representing the entire world
		private static final long WORLDWIDE_WOE = 1L;
		
		private final Twitter twitter;

		@Inject
		public TrendsController(Twitter twitter) {
			this.twitter = twitter;
		}

		@RequestMapping(value="/twitter/trends", method=RequestMethod.GET)
		public String showTrends(Model model) {

			model.addAttribute("twitterProfile", twitter.userOperations().getUserProfile());
			model.addAttribute("trends", twitter.searchOperations().getLocalTrends(WORLDWIDE_WOE));
			return "twitter/trends";
		}
}

y vemos aquí el resultado:

Captura de pantalla 2015-10-22 a las 22.58.02 Y esto ha sido todo lo que queríamos abarcar en este post. Todo el código de este ejemplo lo podéis encontrar aquí.

Para más información podéis acceder a :

Publicado en security, social, spring | Etiquetado , , , , | Deja un comentario