Vamos a mejorar nuestro Timeline añadiendo AJAX para evitar la recarga completa de la página cuando se envía un nuevo tweet. El objetivo es enviar un tweet mediante AJAX y si todo va bien, consultar de nuevo el timeline y actualizarlo.

Vamos a crear una API para nuestra aplicación, que nos permita desde javascript hacer las consultas adecuadas. Como formato para las respuesta vamos a utilizar JSON, por su facilidad para trabajar con él desde javascript.

Creando el API

Vamos a crear un controlador nuevo que se va tener las acciones disponibles en nuestra API.

Empezamos con un método que nos permite ver todos los tweets que ha escrito un usuario

app/controllers/Api.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package controllers;

import java.util.List;

import models.Tweet; import models.User; import play.mvc.Controller;

public class Api extends Controller {

<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">tweetsAll</span><span class="o">(){</span>
    <span class="n">List</span><span class="o">&lt;</span><span class="n">Tweet</span><span class="o">&gt;</span> <span class="n">tweets</span> <span class="o">=</span> <span class="n">Tweet</span><span class="o">.</span><span class="na">find</span><span class="o">(</span><span class="s">"order by date desc"</span><span class="o">).</span><span class="na">fetch</span><span class="o">();</span>
    <span class="n">renderJSON</span><span class="o">(</span><span class="n">tweets</span><span class="o">);</span>
<span class="o">}</span>

}

Con el método renderJSON estamos transformando nuestra lista de tweets a formato JSON. Este método hace un recorrido en profundidad.

Añadimos la ruta al fichero de rutas

conf/routes

GET     /api/tweets/all              Api.tweetsAll

Para probar que funciona correctamente entramos en http://localhost:9000/api/tweets/all.

Si nos fijamos en la respuesta, vemos que se muestran todos y cada uno de los campos. Se muestra por ejemplo el campo contraseña, algo no demasiado seguro y en la lista de usuarios que sigue alguien, se muestran todos los datos de ese usuario, incluso los usuarios a los que sigue.

Problema de referencia circular

¿Qué pasa si el user1 sigue al user2 y el user2 sigue al user1?

Vamos a hacer una modificación rápida en el código para probarlo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package controllers;

import java.util.List;

import models.Tweet; import models.User; import play.mvc.Controller;

public class Api extends Controller {

<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">tweetsAll</span><span class="o">(){</span>
    <span class="n">User</span> <span class="n">user1</span> <span class="o">=</span> <span class="n">User</span><span class="o">.</span><span class="na">find</span><span class="o">(</span><span class="s">"byUsername"</span><span class="o">,</span> <span class="s">"user1"</span><span class="o">).</span><span class="na">first</span><span class="o">();</span>
    <span class="n">User</span> <span class="n">user2</span> <span class="o">=</span> <span class="n">User</span><span class="o">.</span><span class="na">find</span><span class="o">(</span><span class="s">"byUsername"</span><span class="o">,</span> <span class="s">"user2"</span><span class="o">).</span><span class="na">first</span><span class="o">();</span>
    <span class="n">user1</span><span class="o">.</span><span class="na">follows</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">user2</span><span class="o">);</span>
    <span class="n">user2</span><span class="o">.</span><span class="na">follows</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">user1</span><span class="o">);</span>
    <span class="n">user1</span><span class="o">.</span><span class="na">save</span><span class="o">();</span>
    <span class="n">user2</span><span class="o">.</span><span class="na">save</span><span class="o">();</span>

    <span class="n">List</span><span class="o">&lt;</span><span class="n">Tweet</span><span class="o">&gt;</span> <span class="n">tweets</span> <span class="o">=</span> <span class="n">Tweet</span><span class="o">.</span><span class="na">find</span><span class="o">(</span><span class="s">"order by date desc"</span><span class="o">).</span><span class="na">fetch</span><span class="o">();</span>
    <span class="n">renderJSON</span><span class="o">(</span><span class="n">tweets</span><span class="o">);</span>
<span class="o">}</span>

}

Consultamos los tweets de user1

http://localhost:9000/api/tweets/all

y

Error de referencia circular

Resolviendo el problema de referencia circulares

Para resolver nuestro problema lo que tenemos que hacer es seleccionar la información que vamos a devolver. Del autor vamos a devolver únicamente su username y no su contraseña ni la lista de personas a las que sigue.

Por defecto play utilizar la librería GSON para convertir a JSON. Esta librería permite escribir Serializer personalizados. El código queda un poco engorroso, así que vamos a utilizar otra librería que en mi opinión tiene una mejor interfaz, flexjson.

Lo primero que debemos hacer es añadir la dependencias de la librería nueva

1
2
3
4
5
6
  # Application dependencies

require:
    - play
    - play -&gt; secure
    - net.sf.flexjson -&gt; flexjson 2.2-SNAPSHOT<span class="w">

Ejecutamos

play dependencies
play eclipsify

Con el comando de dependencias play se descargará automáticamente la librería del repositorio Central de Maven.

Actualiza el proyecto en Eclipse y reinicia el servidor.

Modifica el método para filtrar los campos que se devuelven

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package controllers;

import java.util.List;

import flexjson.JSONSerializer;

import models.Tweet; import models.User; import play.mvc.Controller;

public class Api extends Controller {

<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">tweetsAll</span><span class="o">(){</span>
    <span class="n">List</span><span class="o">&lt;</span><span class="n">Tweet</span><span class="o">&gt;</span> <span class="n">tweets</span> <span class="o">=</span> <span class="n">Tweet</span><span class="o">.</span><span class="na">find</span><span class="o">(</span><span class="s">"order by date desc"</span><span class="o">).</span><span class="na">fetch</span><span class="o">();</span>
    <span class="n">renderJSON</span><span class="o">(</span><span class="k">new</span> <span class="n">JSONSerializer</span><span class="o">().</span><span class="na">include</span><span class="o">(</span><span class="s">"msg"</span><span class="o">,</span> <span class="s">"date"</span><span class="o">,</span> <span class="s">"author.username"</span><span class="o">).</span><span class="na">exclude</span><span class="o">(</span><span class="s">"*"</span><span class="o">).</span><span class="na">serialize</span><span class="o">(</span><span class="n">tweets</span><span class="o">));</span>
<span class="o">}</span>

}

Prueba de nuevo la consulta

http://localhost:9000/api/tweetsFromUser/user1

Añadiendo javascript a la vista

Para la vista vamos a utilizar jQuery y Knockout.js.

Implementación

Primer paso: Trabajar con dependentArray

Lo primero que vamos a hacer es sentar las bases con knockout.js y vamos a implementar de nuevo nuestro modelo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
    var Tweet = function(msg, date, author){
        this.msg = msg;
        this.date = date;
        this.author = author;
    }

<span class="kd">var</span> <span class="nx">viewModel</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">tweets</span> <span class="p">:</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observableArray</span><span class="p">([</span>
        <span class="k">new</span> <span class="nx">Tweet</span><span class="p">(</span><span class="s2">"mensaje1"</span><span class="p">,</span> <span class="s2">"01-03-2010"</span><span class="p">,</span> <span class="s2">"user1"</span><span class="p">),</span>
        <span class="k">new</span> <span class="nx">Tweet</span><span class="p">(</span><span class="s2">"mensaje2"</span><span class="p">,</span> <span class="s2">"02-03-2010"</span><span class="p">,</span> <span class="s2">"user2"</span><span class="p">),</span>
    <span class="p">])</span>
<span class="p">}</span>

<span class="nx">ko</span><span class="p">.</span><span class="nx">applyBindings</span><span class="p">(</span><span class="nx">viewModel</span><span class="p">);</span>

</script>

En nuestro modelo tenemos los tweets, que son un observable array con dos Tweets de prueba.

Ahora necesitamos una plantilla para pintar esos tweets en la pantalla.

1
2
3
4
5
6
7
<script type=“text/html” id=“renderTweet”>
<div class=“tweet”>
    <div class=“tweet-author”></div>
    <div class=“tweet-msg”></div>
    <div class=“tweet-date”></div>
</div>
</script>

Utilizamos la misma estrategia que usamos anteriormente cuando creamos el tag. Pero esta vez estamos haciendo una plantilla para utilizar con jquery tmpl.

Para probar que todo funciona http://localhost:9000/ y debería aparecer el mensaje1 y el mensaje2.

El objetivo del dependentArray es que cuando añadamos un tweet nuevo al array. La interfaz gráfica se actualice automáticamente. Para probarlo tenemos que abrir la consola javascript (En Chrome Menu/Herramientas/Consola Javascript).

Añadimos un nuevo tweet al array

1
viewModel.tweets.push(new Tweet(‘tweet3’,‘03-03-2010’, ‘user3’))

Consola javascript de Chrome

Segundo paso: Poblando el array mediante AJAX

Añadimos una nueva función que se va a encargar de hacer una petición y añadir los tweets de la respuesta.

1
2
3
4
5
6
7
8
viewModel.updateTweets = function(){
    $.get(‘@{Api.tweetsAll()}’, function(data){
        var tweets = $.map(data, function(item){
            return new Tweet(item.msg, item.date, item.author.username)
        })
        viewModel.tweets(tweets);
    });
}

Añadimos una llamada a la función cuando la página se ha cargado

1
2
3
$(function(){
    viewModel.updateTweets();
});

Tercer paso: Enviando tweets mediante AJAX

Añade un nuevo método a la API que permita enviar tweets y nos devuelva el tweet creado en caso de que todo fue bien.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void tweetsNew(String msg){
    Tweet t = new Tweet(msg, Security.userConnected());
    t.validateAndSave();
    ApiResponse resp = new ApiResponse();
    if(validation.hasErrors()){
        resp.status = “ERROR”;
        resp.message = Utils.join(validation.errors(), “,”);
    }else{
        resp.status = “OK”;
        resp.result = t;
    }
    JSONSerializer serializer = new JSONSerializer().include(“status”, “message”, “result.msg”, “result.date”, “result.author.username”).exclude(“*”);
    renderJSON(serializer.serialize(resp));
}

Crea la clase ApiResponse que nos va a permitir encapsular las respuesta de nuestra API. Por si se producen error poder tener un lugar donde enviar los mensajes de error.

1
2
3
4
5
6
7
package responses;

public class ApiResponse { public String status; public String message; public Object result; }

Añade la ruta

GET     /api/tweets/new                              Api.tweetsNew

Modifica la vista

La función createTweet se encarga de hacer la petición a la acción tweetsNew pasándole por parámetro el mensaje del tweet nuevo. Si la respuesta fue correcta lo añade a la lista de tweets. Si hubo error lo almacena en el modelo para actualizar la interfaz.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function parseTweet(tweet){
    return new Tweet(tweet.msg, new Date(tweet.date), tweet.author.username);
}

viewModel.createTweet = function(){ var action = #{jsAction @Api.tweetsNew(‘:msg’)/} $.get(action({msg : viewModel.newTweet()}), function(data){ if(data.status === ‘OK’){ viewModel.tweets.unshift(parseTweet(data.result)); viewModel.newTweet(‘’); viewModel.clearError(); }else{ viewModel.error(data.message); } }); return false; }

Cambiamos nuestro formulario, para que al hacer submit, se ejecute la función createTweet.

1
2
3
4
5
6
<form data-bind=“submit:createTweet”>
    <div class=“clearfix”>
        <textarea class=“xxlarge” name=“tweet” rows=“3” data-bind=“value:newTweet”></textarea>
    </div>
    <input type=“submit” class=“btn primary” value=“Tweet”>
</form>

Ponemos el código para mostrar los errores

1
2
3
4
<div class=“alert-message error” data-bind=“visible: error() != null”>
    <a class=“close” href=“#” data-bind=“click: clearError”>×</a>
    <p data-bind=“text: error”></p>
</div>

Añadimos una función para limpiar los errores. Esta función se ejecutará cuando en tweet se envíe correctamente o cuando el usuario pulse la ‘x’ del mensaje.

1
2
3
4
viewModel.clearError = function(){
    viewModel.error(null);
    return false;
}

La aspecto final de la vista es

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
86
87
88
#{extends ‘main.html’ /}

{set title:‘Timeline’ /}

{set ‘secondaryContent’}

<ul> <li>Following: </li> <li>Followers: </li> </ul>

{/set}

<div class=“alert-message error” data-bind=“visible: error() != null”> <a class=“close” href=“#” data-bind=“click: clearError”>×</a> <p data-bind=“text: error”></p> </div>

<form data-bind=“submit:createTweet”> <div class=“clearfix”> <textarea class=“xxlarge” name=“tweet” rows=“3” data-bind=“value:newTweet”></textarea> </div> <input type=“submit” class=“btn primary” value=“Tweet”> </form>

<div data-bind=“template :{name:‘renderTweet’, foreach:tweets}”>

</div>

<script type=“text/html” id=“renderTweet”> <div class=“tweet”> <div class=“tweet-author”></div> <div class=“tweet-msg”></div> <div class=“tweet-date”></div> </div> </script>

<script> var Tweet = function(msg, date, author){ this.msg = msg; this.date = date; this.author = author; }

<span class="kd">var</span> <span class="nx">viewModel</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">newTweet</span> <span class="p">:</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observable</span><span class="p">(</span><span class="s1">''</span><span class="p">),</span>
    <span class="na">error</span> <span class="p">:</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observable</span><span class="p">(</span><span class="kc">null</span><span class="p">),</span>
    <span class="na">tweets</span> <span class="p">:</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observableArray</span><span class="p">([])</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">parseTweet</span><span class="p">(</span><span class="nx">tweet</span><span class="p">){</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nx">Tweet</span><span class="p">(</span><span class="nx">tweet</span><span class="p">.</span><span class="nx">msg</span><span class="p">,</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">tweet</span><span class="p">.</span><span class="nx">date</span><span class="p">),</span> <span class="nx">tweet</span><span class="p">.</span><span class="nx">author</span><span class="p">.</span><span class="nx">username</span><span class="p">);</span>
<span class="p">}</span>

<span class="nx">viewModel</span><span class="p">.</span><span class="nx">updateTweets</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(){</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">'@{Api.tweetsAll()}'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>
        <span class="kd">var</span> <span class="nx">tweets</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">){</span>
            <span class="k">return</span> <span class="nx">parseTweet</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>
        <span class="p">})</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">tweets</span><span class="p">(</span><span class="nx">tweets</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">}</span>

<span class="nx">viewModel</span><span class="p">.</span><span class="nx">createTweet</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(){</span>
    <span class="kd">var</span> <span class="nx">action</span> <span class="o">=</span> <span class="err">#</span><span class="p">{</span><span class="nx">jsAction</span> <span class="err">@</span><span class="nx">Api</span><span class="p">.</span><span class="nx">tweetsNew</span><span class="p">(</span><span class="s1">':msg'</span><span class="p">)</span><span class="o">/</span><span class="p">}</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">action</span><span class="p">({</span><span class="na">msg</span> <span class="p">:</span> <span class="nx">viewModel</span><span class="p">.</span><span class="nx">newTweet</span><span class="p">()}),</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>
        <span class="k">if</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'OK'</span><span class="p">){</span>
            <span class="nx">viewModel</span><span class="p">.</span><span class="nx">tweets</span><span class="p">.</span><span class="nx">unshift</span><span class="p">(</span><span class="nx">parseTweet</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">));</span>
            <span class="nx">viewModel</span><span class="p">.</span><span class="nx">newTweet</span><span class="p">(</span><span class="s1">''</span><span class="p">);</span>
            <span class="nx">viewModel</span><span class="p">.</span><span class="nx">clearError</span><span class="p">();</span>
        <span class="p">}</span><span class="k">else</span><span class="p">{</span>
            <span class="nx">viewModel</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">});</span>
    <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">viewModel</span><span class="p">.</span><span class="nx">clearError</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(){</span>
    <span class="nx">viewModel</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
    <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">ko</span><span class="p">.</span><span class="nx">applyBindings</span><span class="p">(</span><span class="nx">viewModel</span><span class="p">);</span>

<span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
    <span class="nx">viewModel</span><span class="p">.</span><span class="nx">updateTweets</span><span class="p">();</span>
<span class="p">});</span>

</script>

Ejercicio

Hasta ahora nuestra aplicación no es más que una especie de tablón de anuncios donde los usuarios pueden escribir sus mensajes. Vamos a dotarle de las funcionalidades de twitter.

Solución

Para mostrar la información de el número de personas que sigue y que son seguidas por un usuario vamos a modificar la entidad User y añadir un nuevo método en nuestra API.

app/models/User.java

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
@Entity
public class User extends Model {
    

<span class="nd">@Transient</span>
<span class="kd">public</span> <span class="n">Long</span> <span class="n">followsNumber</span><span class="o">;</span>

<span class="nd">@Transient</span>
<span class="kd">public</span> <span class="n">Long</span> <span class="n">followersNumber</span><span class="o">;</span>


<span class="cm">/**
 * Cuenta el número de persona que sigue un usuario
 * @return
 */</span>
<span class="kd">public</span> <span class="n">Long</span> <span class="nf">getFollowsNumber</span><span class="o">(){</span>
    <span class="k">if</span><span class="o">(</span><span class="n">followsNumber</span> <span class="o">==</span> <span class="kc">null</span><span class="o">){</span>
        <span class="n">followsNumber</span> <span class="o">=</span> <span class="n">User</span><span class="o">.</span><span class="na">count</span><span class="o">(</span><span class="s">"select count(follow) from User user join user.follows follow where user = ?"</span><span class="o">,</span> <span class="k">this</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">followsNumber</span><span class="o">;</span>      
<span class="o">}</span>

<span class="cm">/**
 * Cuenta el número de persona que siguen a un usuario
 * @return
 */</span>
<span class="kd">public</span> <span class="n">Long</span> <span class="nf">getFollowersNumber</span><span class="o">(){</span>
    <span class="k">if</span><span class="o">(</span><span class="n">followersNumber</span> <span class="o">==</span> <span class="kc">null</span><span class="o">){</span>
        <span class="n">followersNumber</span> <span class="o">=</span> <span class="n">User</span><span class="o">.</span><span class="na">count</span><span class="o">(</span><span class="s">"select count(user) from User user where ? member of user.follows"</span><span class="o">,</span> <span class="k">this</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">followersNumber</span><span class="o">;</span>
<span class="o">}</span>    

}

La anotación @Transient permite que un campo no se almacene en base de datos. Sirve para indicar campos calculados.

app/controller/Api.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Api extends Controller {
    

<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">userInfo</span><span class="o">(</span><span class="n">String</span> <span class="n">username</span><span class="o">){</span>
    <span class="n">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="o">.</span><span class="na">find</span><span class="o">(</span><span class="s">"byUsername"</span><span class="o">,</span> <span class="n">username</span><span class="o">).</span><span class="na">first</span><span class="o">();</span>
    <span class="n">ApiResponse</span> <span class="n">resp</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ApiResponse</span><span class="o">();</span>
    <span class="k">if</span><span class="o">(</span><span class="n">user</span><span class="o">!=</span> <span class="kc">null</span><span class="o">){</span>
        <span class="n">resp</span><span class="o">.</span><span class="na">status</span> <span class="o">=</span> <span class="s">"OK"</span><span class="o">;</span>
        <span class="n">resp</span><span class="o">.</span><span class="na">result</span> <span class="o">=</span> <span class="n">user</span><span class="o">;</span>
    <span class="o">}</span><span class="k">else</span><span class="o">{</span>
        <span class="n">resp</span><span class="o">.</span><span class="na">status</span> <span class="o">=</span> <span class="s">"ERROR"</span><span class="o">;</span>
        <span class="n">resp</span><span class="o">.</span><span class="na">message</span> <span class="o">=</span> <span class="s">"User not found"</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="n">renderJSON</span><span class="o">(</span><span class="k">new</span> <span class="n">JSONSerializer</span><span class="o">().</span><span class="na">include</span><span class="o">(</span><span class="s">"status"</span><span class="o">,</span> <span class="s">"message"</span><span class="o">,</span> <span class="s">"result.username"</span><span class="o">,</span> <span class="s">"result.followsNumber"</span><span class="o">,</span> <span class="s">"result.followersNumber"</span><span class="o">).</span><span class="na">exclude</span><span class="o">(</span><span class="s">"*"</span><span class="o">).</span><span class="na">serialize</span><span class="o">(</span><span class="n">resp</span><span class="o">));</span>
<span class="o">}</span>

}

app/conf/routes

1
GET     /api/user/{username}                         Api.userInfo

Pruebas

http://localhost:9000/api/user/user1

{"message":null,"result":{"followersNumber":3,"followsNumber":0,"username":"user1"},"status":"OK"}

http://localhost:9000/api/user/user3

{"message":null,"result":{"followersNumber":1,"followsNumber":2,"username":"user3"},"status":"OK"}

http://localhost:9000/api/user/user10

{"message":"User not found","result":null,"status":"ERROR"}

apps/views/Timeline/index.html

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
#{set ‘secondaryContent’}

<div> <h3 data-bind=“text: userInfo().username”></h3> <ul> <li>Following: <span data-bind=“text: userInfo().following”></span></li> <li>Followers: <span data-bind=“text: userInfo().followers”></span></li> </ul> </div>

{/set}

….

<script type=“text/html” id=“renderTweet”> <div class=“tweet”> <div class=“tweet-author”><a href=“#” data-bind=“click: function(){ viewModel.username(author) }”></a></div> <div class=“tweet-msg”></div> <div class=“tweet-date”></div> </div> </script>

….

{set ‘endScript’}

<script>

var UserInfo = function(username, following, followers){ this.username = username; this.following = following; this.followers = followers; }

var viewModel = { newTweet : ko.observable(‘’), error : ko.observable(null), tweets : ko.observableArray([]), connectedUser : ‘${controllers.Security.userConnected().username}’, username : ko.observable(‘’), userInfo : ko.observable(new UserInfo(“”, 0, 0)) }

ko.linkObservableToUrl(viewModel.username, “user”, ‘${controllers.Security.userConnected().username}’);

viewModel.username.subscribe(function(username) { var action = #{jsAction @Api.userInfo(‘:username’)/}; $.get(action({username : username}), function(data){ if(data.status === ‘OK’){ viewModel.userInfo(new UserInfo(data.result.username, data.result.followsNumber, data.result.followersNumber)); }else{ viewModel.error(data.message); } }); });

{/set}

Vamos a añadir nuevos métodos que muestran el Timeline de un usuario y los tweets de un usuario en concerto.

apps/controllers/Api.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void timeline(){
    List<Object> tweets = Tweet.find(“select tweet from Tweet tweet, User user where user = ? and (tweet.author = user or tweet.author member of user.follows) order by tweet.date desc”, Security.userConnected()).fetch();
    ApiResponse resp = new ApiResponse();
    resp.status = “OK”;
    resp.result = tweets;
    renderJSON(tweetsSerializer.serialize(resp));
}

public static void tweetsFromUser(String username){
List<Tweet> tweets = Tweet.find(“select tweet from Tweet tweet where tweet.author.username = ? order by tweet.date desc”, username).fetch(); ApiResponse resp = new ApiResponse(); resp.status = “OK”; resp.result = tweets; renderJSON(tweetsSerializer.serialize(resp)); }

apps/views/Timeline/index.html

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
viewModel.username.subscribe(function(user) {
    var viewTimeline = false;
    var username = user;

<span class="k">if</span><span class="p">(</span><span class="k">typeof</span> <span class="nx">username</span> <span class="o">==</span> <span class="s1">'undefined'</span> <span class="o">||</span> <span class="nx">username</span> <span class="o">===</span> <span class="s1">''</span><span class="p">){</span>
    <span class="kd">var</span> <span class="nx">viewTimeline</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">username</span> <span class="o">=</span> <span class="s1">'${controllers.Security.userConnected().username}'</span><span class="p">;</span>            
<span class="p">}</span>

<span class="kd">var</span> <span class="nx">action</span> <span class="o">=</span> <span class="err">#</span><span class="p">{</span><span class="nx">jsAction</span> <span class="err">@</span><span class="nx">Api</span><span class="p">.</span><span class="nx">userInfo</span><span class="p">(</span><span class="s1">':username'</span><span class="p">)</span><span class="o">/</span><span class="p">};</span>

<span class="c1">//Información del usuario</span>
<span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">action</span><span class="p">({</span><span class="na">username</span> <span class="p">:</span> <span class="nx">username</span><span class="p">}),</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'OK'</span><span class="p">){</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">userInfo</span><span class="p">(</span><span class="k">new</span> <span class="nx">UserInfo</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">username</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">followsNumber</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">followersNumber</span><span class="p">));</span>
    <span class="p">}</span><span class="k">else</span><span class="p">{</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="c1">//Timeline o tweets de un usuario concreto</span>
<span class="kd">var</span> <span class="nx">url</span><span class="p">;</span>
<span class="k">if</span><span class="p">(</span><span class="nx">viewTimeline</span><span class="p">){</span>
    <span class="c1">//Timeline</span>
    <span class="nx">url</span> <span class="o">=</span> <span class="s1">'@{Api.timeline}'</span><span class="p">;</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'url timeline'</span> <span class="o">+</span> <span class="nx">url</span><span class="p">);</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
    <span class="c1">//Tweets de un usuario</span>
    <span class="kd">var</span> <span class="nx">action</span> <span class="o">=</span> <span class="err">#</span><span class="p">{</span><span class="nx">jsAction</span> <span class="err">@</span><span class="nx">Api</span><span class="p">.</span><span class="nx">tweetsFromUser</span><span class="p">(</span><span class="s1">':username'</span><span class="p">)</span><span class="o">/</span><span class="p">};</span>
    <span class="nx">url</span> <span class="o">=</span> <span class="nx">action</span><span class="p">({</span><span class="na">username</span> <span class="p">:</span> <span class="nx">username</span><span class="p">});</span>
<span class="p">}</span>

<span class="c1">//Recupera los tweets</span>
<span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>   
    <span class="k">if</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'OK'</span><span class="p">){</span>
        <span class="kd">var</span> <span class="nx">tweets</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">){</span>
            <span class="k">return</span> <span class="nx">parseTweet</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>
        <span class="p">})</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">tweets</span><span class="p">(</span><span class="nx">tweets</span><span class="p">);</span>
    <span class="p">}</span><span class="k">else</span><span class="p">{</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>  

});

Por último vamos a añadir los botones de follow y unfollow.

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
public static void follow(String user){
    User usuarioConectado = Security.userConnected();
    User usuarioConsultado = User.find(“byUsername”, user).first();

<span class="n">usuarioConectado</span><span class="o">.</span><span class="na">follows</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">usuarioConsultado</span><span class="o">);</span>
<span class="n">usuarioConectado</span><span class="o">.</span><span class="na">save</span><span class="o">();</span>

<span class="n">ApiResponse</span> <span class="n">resp</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ApiResponse</span><span class="o">();</span>
<span class="n">resp</span><span class="o">.</span><span class="na">status</span> <span class="o">=</span> <span class="s">"OK"</span><span class="o">;</span>
<span class="n">renderJSON</span><span class="o">(</span><span class="n">statusSerializer</span><span class="o">.</span><span class="na">serialize</span><span class="o">(</span><span class="n">resp</span><span class="o">));</span>

}

public static void unfollow(String user){ User usuarioConectado = Security.userConnected(); User usuarioConsultado = User.find(“byUsername”, user).first();

<span class="n">usuarioConectado</span><span class="o">.</span><span class="na">follows</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">usuarioConsultado</span><span class="o">);</span>
<span class="n">usuarioConectado</span><span class="o">.</span><span class="na">save</span><span class="o">();</span>

<span class="n">ApiResponse</span> <span class="n">resp</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ApiResponse</span><span class="o">();</span>
<span class="n">resp</span><span class="o">.</span><span class="na">status</span> <span class="o">=</span> <span class="s">"OK"</span><span class="o">;</span>
<span class="n">renderJSON</span><span class="o">(</span><span class="n">statusSerializer</span><span class="o">.</span><span class="na">serialize</span><span class="o">(</span><span class="n">resp</span><span class="o">));</span>

}

public static void isFollowing(String user){ User usuarioConectado = Security.userConnected(); User usuarioConsultado = User.find(“byUsername”, user).first();

<span class="n">ApiResponse</span> <span class="n">resp</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ApiResponse</span><span class="o">();</span>
<span class="n">resp</span><span class="o">.</span><span class="na">status</span> <span class="o">=</span> <span class="s">"OK"</span><span class="o">;</span>
<span class="n">resp</span><span class="o">.</span><span class="na">result</span> <span class="o">=</span> <span class="n">usuarioConectado</span><span class="o">.</span><span class="na">follows</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">usuarioConsultado</span><span class="o">);</span>
<span class="n">renderJSON</span><span class="o">(</span><span class="n">statusSerializer</span><span class="o">.</span><span class="na">serialize</span><span class="o">(</span><span class="n">resp</span><span class="o">));</span>

}

1
2
3
4
5
6
7
8
9
<div data-bind=“visible: viewTimeline() == false”>
    <div data-bind=“visible : connectedFollowingUsername() == false”>
        <a href=“#” class=“btn” data-bind=“click: followUser”>Follow</a>
    </div>

<span class="nt">&lt;div</span> <span class="na">data-bind=</span><span class="s">"visible : connectedFollowingUsername"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"btn"</span> <span class="na">data-bind=</span><span class="s">"click: unfollowUser"</span><span class="nt">&gt;</span>Unfollow<span class="nt">&lt;/a&gt;</span>
<span class="nt">&lt;/div&gt;</span>

</div>

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
viewModel.username.subscribe(refresh);

function refresh(user){ var username; var connectedUser; if(typeof user == ‘undefined’ || user === ‘’){ viewModel.viewTimeline(true); username = ‘${controllers.Security.userConnected().username}’;
}else{ viewModel.viewTimeline(false); username = user; }

<span class="kd">var</span> <span class="nx">action</span> <span class="o">=</span> <span class="err">#</span><span class="p">{</span><span class="nx">jsAction</span> <span class="err">@</span><span class="nx">Api</span><span class="p">.</span><span class="nx">userInfo</span><span class="p">(</span><span class="s1">':username'</span><span class="p">)</span><span class="o">/</span><span class="p">};</span>

<span class="c1">//Información del usuario</span>
<span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">action</span><span class="p">({</span><span class="na">username</span> <span class="p">:</span> <span class="nx">username</span><span class="p">}),</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'OK'</span><span class="p">){</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">userInfo</span><span class="p">(</span><span class="k">new</span> <span class="nx">UserInfo</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">username</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">followsNumber</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">followersNumber</span><span class="p">));</span>
    <span class="p">}</span><span class="k">else</span><span class="p">{</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="c1">//Timeline o tweets de un usuario concreto</span>
<span class="kd">var</span> <span class="nx">url</span><span class="p">;</span>
<span class="k">if</span><span class="p">(</span><span class="nx">viewModel</span><span class="p">.</span><span class="nx">viewTimeline</span><span class="p">()){</span>
    <span class="c1">//Timeline</span>
    <span class="nx">url</span> <span class="o">=</span> <span class="s1">'@{Api.timeline}'</span><span class="p">;</span>
<span class="p">}</span><span class="k">else</span><span class="p">{</span>
    <span class="c1">//Tweets de un usuario</span>
    <span class="kd">var</span> <span class="nx">action</span> <span class="o">=</span> <span class="err">#</span><span class="p">{</span><span class="nx">jsAction</span> <span class="err">@</span><span class="nx">Api</span><span class="p">.</span><span class="nx">tweetsFromUser</span><span class="p">(</span><span class="s1">':username'</span><span class="p">)</span><span class="o">/</span><span class="p">};</span>
    <span class="nx">url</span> <span class="o">=</span> <span class="nx">action</span><span class="p">({</span><span class="na">username</span> <span class="p">:</span> <span class="nx">username</span><span class="p">});</span>
<span class="p">}</span>

<span class="c1">//Recupera los tweets</span>
<span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>   
    <span class="k">if</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'OK'</span><span class="p">){</span>
        <span class="kd">var</span> <span class="nx">tweets</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">){</span>
            <span class="k">return</span> <span class="nx">parseTweet</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>
        <span class="p">})</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">tweets</span><span class="p">(</span><span class="nx">tweets</span><span class="p">);</span>
    <span class="p">}</span><span class="k">else</span><span class="p">{</span>
        <span class="nx">viewModel</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="c1">//Información de si está siguiendo o no al usuario</span>
<span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">viewModel</span><span class="p">.</span><span class="nx">viewTimeline</span><span class="p">()){</span>
    <span class="kd">var</span> <span class="nx">action</span> <span class="o">=</span> <span class="err">#</span><span class="p">{</span><span class="nx">jsAction</span> <span class="err">@</span><span class="nx">Api</span><span class="p">.</span><span class="nx">isFollowing</span><span class="p">(</span><span class="s1">':username'</span><span class="p">)</span><span class="o">/</span><span class="p">};</span>
    <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">action</span><span class="p">({</span><span class="na">username</span> <span class="p">:</span> <span class="nx">username</span><span class="p">});</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>
        <span class="k">if</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'OK'</span><span class="p">){</span>
            <span class="nx">viewModel</span><span class="p">.</span><span class="nx">connectedFollowingUsername</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">result</span><span class="p">);</span>
        <span class="p">}</span><span class="k">else</span><span class="p">{</span>
            <span class="nx">viewModel</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">}</span>    

}

viewModel.followUser = function(){ var action = #{jsAction @Api.follow(‘:username’)/}; var url = action({username : viewModel.username()}); $.get(url, function(data){ if(data.status === ‘OK’){ refresh(viewModel.username()); } }); }

viewModel.unfollowUser = function(){ var action = #{jsAction @Api.unfollow(‘:username’)/}; var url = action({username : viewModel.username()}); $.get(url, function(data){ if(data.status === ‘OK’){ refresh(viewModel.username()); } }); }