O Guia do Desenvolvedor Java Ocupado para o Scala: Scala + Twitter = Scitter

Com o Scala, Você Pode se Relacionar de uma Maneira Mais "Social"

É divertido falar sobre o Scala na teoria, mas para a maioria dos leitores desta coluna, utilizá-lo na prática demonstra a diferença entre vê-lo como um "brinquedo" e utilizá-lo no trabalho.Nesta parte, Ted Neward utiliza o Scala para construir a estrutura básica de uma biblioteca cliente para acessar o Twitter, um sistema popular de microblogging.

Ted Neward, Principal, Neward & Associates

Ted Neward photoTed Neward é consultor da ThoughtWorks, uma empresa de consultoria mundial, e diretor da Neward & Associates, onde oferece serviços como consultor, mentor e professor, além de fazer apresentações sobre Java, .NET, XML Services e outras plataformas. Reside próximo de Seattle, Washington.



05/Mai/2009

O Twitter revolucionou a Internet. Como você já sabe, esta engenhosa ferramenta de rede de relacionamentos permite que seus assinantes apresentem breves atualizações de status sobre si mesmos e o que tem feito ultimamente. Os seguidores recebem atualizações sobre seu "Twitter feed" da mesma maneira que os blogs geram atualizações no feed de leitores do blog.

Sobre esta Série

Ted Neward mergulha de cabeça na linguagem de programação Scala e o leva com ele. Nesta série, você vai aprender do que se trata toda essa propaganda e vai ver alguns dos recursos linguísticos do Scala em ação. O código Scala e o código Java™ serão mostrados lado a lado sempre que uma comparação for relevante, mas (conforme você vai descobrir) muitas coisas no Scala não têm correlação direta com nada que você viu em Java — e esse é o charme do Scala! Afinal de contas, se o Java poderia fazer tudo isso, por que se preocupar em aprender sobre o Scala?

Por si só, o Twitter é uma discussão interessante sobre redes de relacionamentos e sobre uma nova geração de usuários "altamente conectados", completa de todos os prós e contras que você acha que estão associados a ele.

Como o Twitter publicou sua API antecipadamente, inúmeros aplicativos clientes do Twitter explodiram na Internet. Como essa API tem uma base fundamentalmente direta e de fácil entendimento, vários desenvolvedores acharam que seria educativo construir um cliente Twitter próprio da mesma maneira em que os desenvolvedores aprendendo sobre tecnologias da Web construíram seus próprios servidores de blogs como um exercício educacional.

Dada a natureza funcional do Scala (que parecia se alinhar muito bem com a natureza RESTful do Twitter) e os recursos de processamento XML extremamente bons, me pareceu uma boa experiência construir uma biblioteca cliente do Scala para acessar o Twitter.

O que É Twitter?

Antes de prosseguirmos, se você ainda não tiver dado uma olhada na API do Twitter (ou utilizado essa tecnologia), vamos fazer isso agora.

Simplificando, um Twitter é um "microblog" — um feed personalizado de pequenas declarações sobre você mesmo, com no máximo 140 caracteres de comprimento, que qualquer pessoa que queira "seguir" poderá receber via atualizações da Web, RSS, mensagem de texto, entre outros. (O limite de 140 caracteres é necessário apenas porque as mensagens de texto, um dos principais canais de origem do Twitter, são semelhantemente restritas.)

O que É Geralmente RESTful?

Alguns leitores ficarão curiosos quanto ao uso da frase Geralmente RESTful; isso merece uma explicação. "A API do Twitter tenta ser compatível com os princípios de design de Representational State Transfer (REST)." E em grande parte, ela é. Roy Fielding, o originador da ideia, pode discordar do uso do termo por parte do Twitter, mas para ser honesto, a abordagem do Twitter está muito mais próxima da definição das pessoas para o termo REST. Para ser realmente o honesto, só quero evitar comentários maldosos sobre o que é REST. Portanto, vou utilizar o qualificador "geralmente".

A partir de um ponto de vista programático, Twitter é uma API geralmente RESTful na qual você utiliza algum tipo de formato da mensagem — XML, ATOM, RSS ou JSON — para enviar e receber mensagens de servidores Twitter. As diferentes URLs, combinadas com as diferentes mensagens e suas partes de mensagens obrigatórias e opcionais, fazem as diferentes chamadas de API. Por exemplo, se você quiser receber a lista completa de todas as "Tweets" (atualizações do Twitter) de todas as pessoas no Twitter (também conhecido como "linha de tempo pública"), você prepara uma mensagem XML, ATOM, RSS ou JSON, a envia para a URL apropriada e consome o resultado no mesmo formato documentado no Web site do Twitter, apiwiki.twitter.com:

------------------------------------------------------------
public_timeline
Retorna os 20 status mais recentes de usuários não protegidos
que configuraram um ícone de usuário customizado. Não requer autenticação.

Observe que a linha de tempo pública fica armazenada em cache por 60 segundos, portanto,
solicitá-la com mais frequência é um desperdício de recursos.

URL:http://twitter.com/statuses/public_timeline.format
Formatos: xml, json, rss, atom
Método(s): GET
Limite de API: Não aplicável
Retorna: lista de elementos de status
------------------------------------------------------------

A partir de uma perspectiva de programação, isso significa que enviamos um simples pedido GET HTTP para o servidor Twitter e receberemos de volta uma lista de mensagens de "status" envolvidos em uma mensagem XML, RSS, ATOM ou JSON. Uma mensagem de "status" é definida no site do Twitter como algo vagamente semelhante à Listagem 1:

Listagem 1. Mundo, Onde Você Está?
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <title>Twitter / tedneward</title>
  <id>tag:twitter.com,2007:Status</id>
  <link type="text/html" rel="alternate" href="http://twitter.com/tedneward"/>
  <updated>2009-03-07T13:48:31+00:00</updated>
  <subtitle>Twitter updates from Ted Neward / tedneward.</subtitle>
  <entry>
	<title>tedneward: @kdellison Happens to the best of us...</title>
	<content type="html">tedneward: @kdellison Happens to the best of us...</content>
	<id>tag:twitter.com,2007:http://twitter.com/tedneward/statuses/1292396349</id>
	<published>2009-03-07T11:07:18+00:00</published>
	<updated>2009-03-07T11:07:18+00:00</updated>
	<link type="text/html" rel="alternate"
        href="http://twitter.com/tedneward/statuses/1292396349"/>
	<link type="image/png" rel="image"
        href="http://s3.amazonaws.com/twitter_production/profile_images/
         55857457/javapolis_normal.png"/>
	<author>
	  <name>Ted Neward</name>
	  <uri>http://www.tedneward.com</uri>
	</author>
  </entry>
</feed>

A maioria dos elementos (se não todos eles) na mensagem de status é direta, por isso a deixaremos por conta da imaginação.

Como podemos consumir mensagens do Twitter em um dos três formatos baseados em XML e como o Scala possui alguns recursos XML poderosos, incluindo literais XML e APIs de sintaxe de consulta em formato XPath, a gravação de uma biblioteca do Scala que possa enviar e receber mensagens do Twitter é um exercício em algumas codificações básicas do Scala. Por exemplo, o uso do Scala para consumir a mensagem da Listagem 1 para selecionar o título ou o conteúdo da atualização de status pode fazer uso dos tipos XML do Scala e dos métodos \ e \\, mostrados na Listagem 2:

Listagem 2. Ted, Onde Você Está?
<![CDATA[
package com.tedneward.scitter.test
{
  class ScitterTest
  {
    import org.junit._, Assert._

    @Test def simpleAtomParse =
    {
      val atom =
        <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
          <title>Twitter / tedneward</title>
          <id>tag:twitter.com,2007:Status</id>
          <link type="text/html" rel="alternate" href="http://twitter.com/tedneward"/>
          <updated>2009-03-07T13:48:31+00:00</updated>
          <subtitle>Twitter updates from Ted Neward / tedneward.</subtitle>
          <entry>
            <title>tedneward: @kdellison Happens to the best of us...</title>
            <content type="html">tedneward: @kdellison
                                 Happens to the best of us...</content>
            <id>tag:twitter.com,2007:
                http://twitter.com/tedneward/statuses/1292396349</id>
            <published>2009-03-07T11:07:18+00:00</published>
            <updated>2009-03-07T11:07:18+00:00</updated>
            <link type="text/html" rel="alternate"
                  href="http://twitter.com/tedneward/statuses/1292396349"/>
            <link type="image/png" rel="image"
                  href="http://s3.amazonaws.com/twitter_production/profile_images/
                        55857457/javapolis_normal.png"/>
            <author>
              <name>Ted Neward</name>
              <uri>http://www.tedneward.com</uri>
            </author>
          </entry>
        </feed>

      assertEquals(atom \\ "entry" \ "title",
		 "tedneward: @kdellison Happens to the best of us...")
    }
  }
}
]]>

Para obter mais alguns detalhes sobre o suporte XML do Scala, consulte "Scala e XML" (consulte Recursos).

Consumir um XML bruto não é de fato um exercício divertido. Se o Scala foi projetado para facilitar nossas vidas, vamos deixá-lo facilitar nossas vidas criando uma classe ou uma coleta de classes projetadas especificamente para tornar o envio e o recebimento de mensagens do Scala mais fáceis. Como um objetivo adicional, a biblioteca deve ser facilmente consumível a partir de um programa Java "normal" (o que significa que ela estará facilmente acessível a partir de qualquer coisa que compreenda a semântica Java normal, como Groovy ou Clojure).

Design de API

Antes de nos aprofundarmos no design da API para a biblioteca do Scala/Twitter (que chamarei de "Scitter", conforme sugerido por meu colega da ThoughtWorks, Neal Ford), existem alguns requisitos que precisam ser discutidos.

Primeiro, está claro que o Scitter terá algum tipo de dependência no acesso à rede — e, por extensão, os servidores Twitter — o que dificultará seu teste.

Segundo, vamos precisar analisar (e testar) os vários formatos que o Twitter enviar de volta.

Terceiro, queremos ocultar as diferenças entre os vários formatos por trás da API para que o cliente não precise se preocupar com os formatos de mensagens documentados do Twitter, mas possa trabalhar com classes padrão.

E, por fim, como o Twitter conta com o "usuário autenticado" para um grande número de APIs, a biblioteca do Scitter precisará acomodar a diferença entre APIs "autenticadas" e "não autenticadas" conforme apropriado, sem tornar as coisas muito complicadas.

O acesso à rede vai exigir alguma forma de comunicação HTTP para contatar os servidores Twitter. Embora pudéssemos utilizar a biblioteca Java em si (especificamente a classe de URL e suas irmãs), como as APIs do Twitter exigem muito da conexão do corpo do pedido e da resposta, será mais fácil utilizar uma API HTTP diferente, especificamente a biblioteca Apache Commons HttpClient. Para facilitar o teste da API do cliente, a comunicação real ficará por trás de algumas APIs internamente para a biblioteca do Scitter, não só para facilitar o descarregamento para a área de troca para outra biblioteca HTTP (não que eu consiga imaginar porque isso seria necessário), mas também para facilitar a simulação da comunicação de rede real para a realização de testes mais fáceis (cuja necessidade é muito fácil de se imaginar).

Como resultado, o primeiro teste é para simplesmente "Scalaficar" as chamadas HttpClient para garantirmos a existência de um padrão de comunicação básico adequado; observe que como a HttpClient depende de duas outras bibliotecas do Apache (Commons Logging e Commons Codec), ambas as bibliotecas também precisarão estar presentes durante o tempo de execução; para os leitores que procuram desenvolver tipos de códigos semelhantes, certifiquem-se de que as três bibliotecas estejam presentes no caminho de classe.

Porque a API do Twitter mais fácil de usar é a API de teste que está documentada para leitura

Retorna a cadeia "ok" no formato solicitado com um código de status HTTP 200 OK.

Vamos utilizar isso como um tiro inicial para os testes de exploração do Scitter. Isso é documentado como ativo na URL http://twitter.com/help/test.format (em que "format" é "xml" ou "json"; vamos optar por utilizar "xml" por enquanto) e o único método de HTTP de suporte é GET. O código HttpClient grava grande parte de si, como na Listagem 3:

Listagem 3. Twitter PING!
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...

    import org.apache.commons.httpclient._, methods._, params._, cookie._

    @Test def callTwitterTest =
    {
      val testURL = "http://twitter.com/help/test.xml"

      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(testURL)

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)

      val statusLine = method.getStatusLine()

      assertEquals(statusLine.getStatusCode(), 200)
      assertEquals(statusLine.getReasonPhrase(), "OK")
    }
  }
}

Grande parte deste código é o código basico HttpClient — os leitores interessados devem consultar a documentação da API HttpClient para obter mais detalhes. Supondo que uma conexão de rede à Internet pública esteja disponível durante a execução (e que o Twitter não mudou sua API pública), este teste deve ser realizado com sucesso.

Com isso, vamos discutir a primeira parte do cliente Scitter. Isso significa que estamos presos em um ponto sobre a estrutura: Como estruturar os clientes Scitter para tratar de chamadas autenticadas versus não autenticadas. No momento, farei isso de uma maneira clássica do Scala, supondo que a autenticação seja "por objeto" e colocando as chamadas que requerem autenticação em uma definição de classe e as chamadas que não requerem autenticação em uma definição de objeto:

Listagem 4. Scitter.test
package com.tedneward.scitter
{
  /**
   * Objeto para consumir feeds do Twitter "não específicos", como linha de tempo pública.
   * Utilize isso para fazer pedidos não autenticados de feeds do Twitter.
   */
  object Scitter
  {
    import org.apache.commons.httpclient._, methods._, params._, cookie._

    /**
     * Execute ping do servidor para saber se ele está ativo e em execução.
     *
     * Documentações do Twitter dizem:
     * teste
     * Retorna a cadeia "ok" no formato solicitado com um código de status HTTP 200 OK.
     * URL: http://twitter.com/help/test.format
     * Formatos: xml, json
     * Método(s): GET
     */
    def test : Boolean =
    {
      val client = new HttpClient()

      val method = new GetMethod("http://twitter.com/help/test.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)

      val statusLine = method.getStatusLine()
      statusLine.getStatusCode() == 200
    }
  }
  /**
   * Classe para consumir APIs do Twitter de "usuário autenticado". Cada instância é
   * "ligada" a um determinado usuário autenticado no Twitter e irá
   * se comportar de forma compatível (de acordo com a documentação da API do Twitter).
   */
  class Scitter(username : String, password : String)
  {
  }
}

Por enquanto, vamos deixar a abstração de rede de lado — esse será um refinamento adicionado posteriormente quando os testes off-line se tornarem mais importantes. Isso também ajudará a evitar um "excesso de abstração" da comunicação da rede quando tivermos uma ideia melhor de como estamos utilizando exatamente a(s)classe(s) HttpClient.

Como fizemos a distinção entre clientes Twitter autenticados e não autenticados, vamos criar rapidamente um método autenticado. Parece que o Twitter possui uma API para verificar as credenciais de login de um usuário que parece estar tão próximo quanto nós de obter um ping autenticado. Mais uma vez, o código HttpClient será semelhante ao código anterior, com a exceção de que agora temos que passar o nome de usuário e a senha para a API do Twitter também.

Isso nos dá uma noção de como o Twitter autentica usuários. Após uma pesquisa rápida na página da API do Twitter, parece que o Twitter utiliza uma abordagem de autenticação HTTP em estoque, a mesma que qualquer recurso autenticado faz no HTTP. Isso significa que o código HttpClient deve fornecer o nome de usuário e a senha como parte do pedido de HTTP, e não como um corpo em "POST" conforme esperado e mostrado na Listagem 5:

Listagem 5. Twitter, Sou Eu!
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    def testUser = "TwitterUser"
	def testPassword = "TwitterPassword"

    @Test def verifyCreds =
    {
      val client = new HttpClient()

      val verifyCredsURL = "http://twitter.com/account/verify_credentials.xml"
      val method = new GetMethod(verifyCredsURL)

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))

      client.getParams().setAuthenticationPreemptive(true)
      val defaultcreds = new UsernamePasswordCredentials(testUser, testPassword)
      client.getState().setCredentials(new AuthScope("twitter.com", 80,
                                                     AuthScope.ANY_REALM), defaultcreds)

      client.executeMethod(method)

      val statusLine = method.getStatusLine()

      assertEquals(200, statusLine.getStatusCode())
      assertEquals("OK", statusLine.getReasonPhrase())
    }
  }
}

Observe que para este teste ser bem-sucedido, os campos de nome de usuário e senha terão que ser preenchidos com algo aceitável pelo Twitter — usei meu próprio nome de usuário e senha do Twitter durante o desenvolvimento, mas é óbvio que você terá que usar o seu. É muito simples se registrar para uma nova conta do Twitter, por isso eu presumo que você já tenha uma ou esteja tentando descobrir como obter uma. (Tudo bem... eu espero.)

Quando esta etapa estiver concluída, será muito fácil ver como isso será mapeado para a classe Scitter utilizando os parâmetros de construtor nome de usuário e senha, mostrados na Listagem 6:

Listagem 6. Scitter.verifyCredentials
package com.tedneward.scitter
{
  import org.apache.commons.httpclient._, auth._, methods._, params._

  // ...

  /**
   * Classe para consumir APIs do Twitter de "usuário autenticado". Cada instância é
   * "ligada" a um determinado usuário autenticado no Twitter e irá
   * se comportar de forma compatível (de acordo com a documentação da API do Twitter).
   */
  class Scitter(username : String, password : String)
  {
    /**
     * Verifique as credenciais do usuário com relação ao Twitter.
     *
     * Documentações do Twitter dizem:
     * verify_credentials
     * Retorna um código de resposta HTTP 200 OK e uma representação do
     * usuário solicitante se a autenticação foi bem-sucedida; retorna um código de status
     * 401 e uma mensagem de erro, caso contrário.  Utilize esse método para testar se as
     * credenciais de usuário fornecidas são válidas.
     * URL: http://twitter.com/account/verify_credentials.format
     * Formatos: xml, json
     * Método(s): GET
     */
    def verifyCredentials : Boolean =
    {
      val client = new HttpClient()

      val method = new GetMethod("http://twitter.com/help/test.xml")
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))

      client.getParams().setAuthenticationPreemptive(true)
      val creds = new UsernamePasswordCredentials(username, password)
      client.getState().setCredentials(
        new AuthScope("twitter.com", 80, AuthScope.ANY_REALM), creds)

      client.executeMethod(method)

      val statusLine = method.getStatusLine()
      statusLine.getStatusCode() == 200
    }
  }
}

E o teste da classe Scitter correspondente na Listagem 7 também é muito simples:

Listagem 7. Testando Scitter.verifyCredentials
package com.tedneward.scitter.test
{
  class ScitterTests
  {
    import org.junit._, Assert._
    import com.tedneward.scitter._

    def testUser = "TwitterUsername"
      def testPassword = "TwitterPassword"

      // ...
	
    @Test def verifyCreds =
    {
      val scitter = new Scitter(testUser, testPassword)
      val result = scitter.verifyCredentials
      assertTrue(result)
    }
  }
}

Nada mau. A estrutura básica da biblioteca está começando a ter uma forma, embora esteja claro que ainda há um longo caminho a ser percorrido, principalmente porque nada do que foi feito até aqui é realmente específico do Scala — a construção da biblioteca foi mais um exercício de design orientado a objetos do que qualquer outra coisa. Portanto, vamos começar a utilizar um pouco de XML e retorná-lo de uma forma mais agradável.

De XML para Objetos

A API mais fácil de se incluir no momento é a public_timeline, que coleta as n atualizações mais recentes que o Twitter recebeu através de todos os usuários e as retorna para uso. Ao contrário das outras duas APIs vistas até agora, a API public_timeline retorna um corpo de resposta (em vez de contar apenas com códigos de status), portanto, precisaremos separar o XML/RSS/ATOM/whatever resultante antes de retorná-lo ao cliente Scitter.

Acompanhando o progresso até aqui, vamos gravar um teste de exploração que atinja o feed público e descarregue os resultados em stdout para exame, conforme mostrado na Listagem 8:

Listagem 8. O que Todos Vão Fazer?
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...

    @Test def callTwitterPublicTimeline =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"

      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)

      val statusLine = method.getStatusLine()
      assertEquals(statusLine.getStatusCode(), 200)
      assertEquals(statusLine.getReasonPhrase(), "OK")

      val responseBody = method.getResponseBodyAsString()
      System.out.println("callTwitterPublicTimeline got... ")
      System.out.println(responseBody)
    }
  }
}

Quando executados, os resultados serão diferentes todas as vezes graças ao grande número de usuários nos servidores Twitter públicos, mas geralmente eles serão semelhantes à Listagem 9 no dump do arquivo de texto JUnit:

Listagem 9. Estas São as Tweets de Nossas Vidas
<statuses type="array">
    <status>
      <created_at>Tue Mar 10 03:14:54 +0000 2009</created_at>
      <id>1303777336</id>
      <text>She really is. http://tinyurl.com/d65hmj</text>
      <source><a href="http://iconfactory.com/software/twitterrific">twitterrific</a>
        </source>
      <truncated>false</truncated>
      <in_reply_to_status_id></in_reply_to_status_id>
      <in_reply_to_user_id></in_reply_to_user_id>
      <favorited>false</favorited>
      <user>
          <id>18729101</id>
          <name>Brittanie</name>
          <screen_name>brittaniemarie</screen_name>
          <description>I'm a bright character. I suppose.</description>
          <location>Atlanta or Philly.</location>
          <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/
                             81636505/goodish_normal.jpg</profile_image_url>
          <url>http://writeitdowntakeapicture.blogspot.com</url>
          <protected>false</protected>
          <followers_count>61</followers_count>
      </user>
    </status>
    <status>
      <created_at>Tue Mar 10 03:14:57 +0000 2009</created_at>
      <id>1303777334</id>
      <text>Number 2 of my four life principles.  "Life is fun and rewarding"</text>
      <source>web</source>
      <truncated>false</truncated>
      <in_reply_to_status_id></in_reply_to_status_id>
      <in_reply_to_user_id></in_reply_to_user_id>
      <favorited>false</favorited>
      <user>
          <id>21465465</id>
          <name>Dale Greenwood</name>
          <screen_name>Greeendale</screen_name>
          <description>Vegetarian. Eat and use only organics. 
                       Love helping people become prosperous</description>
          <location>Melbourne Australia</location>
          <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/
                             90659576/Dock_normal.jpg</profile_image_url>
          <url>http://www.4abundance.mionegroup.com</url>
          <protected>false</protected>
          <followers_count>15</followers_count>
       </user>
     </status>
       (A lot more have been snipped)
</statuses>

É óbvio, ao examinar os resultados e as documentações do Twitter, que o resultado da chamada é uma coleta de mensagens de "status" simples em grande parte, com uma estrutura de mensagem consistente. Refutar os resultados utilizando o suporte XML do Scala é um exercício muito simples, mas iremos refiná-lo assim que tivermos os testes básicos para passarmos, como na Listagem 10:

Listagem 10. O Que Todos Vão Fazer?
package com.tedneward.scitter.test
{
  class ExplorationTests
  {
    // ...

    @Test def simplePublicFeedPullAndParse =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"

      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))
      val statusCode = client.executeMethod(method)
      val responseBody = new String(method.getResponseBody())

      val responseXML = scala.xml.XML.loadString(responseBody)
      val statuses = responseXML \\ "status"

      for (n <- statuses.elements)
      {
        n match
        {
          case <status>{ contents @ _*}</status> =>
          {
            System.out.println("Status: ")
            contents.foreach((c) =>
              c match
              {
                case <text>{ t @ _*}</text> =>
                  System.out.println("\tText: " + t.text.trim)
                case <user>{ contents2 @ _* }</user> =>
                {
                  contents2.foreach((c2) =>
                    c2 match
                    {
                      case <screen_name>{ u }</screen_name> =>
                        System.out.println("\tUser: " + u.text.trim)
                      case _ =>
						()
                    }
                  )
                }
                case _ =>
				  ()
              }
            )
          }
          case _ =>
            () // ou, se você preferir, System.out.println("Unrecognized element!")
        }
      }
    }
  }
}

Considerando os padrões de código de exemplo, isso é pouco animador — parece que seguimos um estilo DOM, com navegação em um elemento filho por vez, extração de texto e navegação para outro nó. Poderíamos simplesmente fazer duas consultas no estilo XPath, algo semelhante à Listagem 11:

Listagem 11. Abordagem de Análise Alternativa
for (n <- statuses.elements)
{
     val text = (n \\ "texto").text
       val screenName = (n \\ "usuário" \ "screen_name").text
}

O que certamente seria menor, mas dois problemas básicos surgem daí:

  • Poderíamos estar forçando a biblioteca XML do Scala a atravessar o gráfico uma vez para cada elemento ou subelemento desejado, o que se tornaria lento com o passar do tempo.
  • Ainda estamos lutando com a estrutura da mensagem XML diretamente. Esta é a questão mais importante das duas.

Podemos dizer que isso não será escalável — supondo que, eventualmente, teremos interesse em cada elemento na mensagem de status do Twitter, teremos que extrair cada elemento de cada status individualmente.

O que leva a outro problema, o dos formatos individuais. Lembre-se, o Twitter possui quatro formatos diferentes que os clientes gostam de utilizar, e nós não queremos que os clientes do Scitter conheçam as diferenças entre eles, por isso o Scitter precisa de uma estrutura intermediária que possamos devolver ao cliente para uso posterior, algo que você verá ao longo das linhas da Listagem 12:

Listagem 12. Descrição da Classe Status
abstract class Status
{
  val createdAt : String
  val id : Long
  val text : String
  val source : String
  val truncated : Boolean
  val inReplyToStatusId : Option[Long]
  val inReplyToUserId : Option[Long]
  val favorited : Boolean
  val user : User
}

... e algo semelhante para User, que não repetirei aqui por razões de concisão. Observe que o elemento filho User possui uma advertência interessante — embora exista um tipo de usuário do Twitter, ele possui um "status mais recente" opcional aninhado. As mensagens de status têm um usuário aninhado dentro delas. Nesse caso, para ajudar a evitar alguns dos possíveis problemas recursivos, estou optando por criar um tipo User, que está aninhado dentro do tipo Status para refletir os dados de User que aparecem lá, e vice-versa para o Status aninhado dentro de User, para, especificamente, evitar esse problema. (Pelo menos este é meu plano até descobrir o problema com isso.)

Agora, tendo criado o tipo de objeto representando as mensagens do Twitter, podemos seguir um padrão do Scala comum de desserialização XML: criar uma definição de objeto correspondente que contenha um método fromXml que utiliza um nó XML e o divide em uma instância de objeto, mostrada na Listagem 13:

Listagem 13. Decifrando o XML
  /**
   * Wrapper de objeto para transformar (formatar) em instâncias de Status.
   */
  object Status
  {
    def fromXml(node : scala.xml.Node) : Status =
    {
      new Status {
        val createdAt = (node \ "created_at").text
        val id = (node \ "id").text.toLong
        val text = (node \ "text").text
        val source = (node \ "source").text
        val truncated = (node \ "truncated").text.toBoolean
        val inReplyToStatusId =
          if ((node \ "in_reply_to_status_id").text != "")
            Some((node \"in_reply_to_status_id").text.toLong)
          else
            None
        val inReplyToUserId =
          if ((node \ "in_reply_to_user_id").text != "")
            Some((node \"in_reply_to_user_id").text.toLong)
          else
            None
        val favorited = (node \ "favorited").text.toBoolean
        val user = User.fromXml((node \ "user")(0))
      }
    }
  }

O bom desse idioma em particular é que ele pode ser estendido para qualquer outro formato de idioma suportado pelo Twitter — se o método fromXml em si poderia verificar se o nó possui um tipo de conteúdo XML, RSS ou Atom antes de dividi-lo, ou se Status pode ter os métodos fromXml, fromRss, fromAtom e fromJson. A última abordagem é, de fato, minha preferência, pois ela trata os formatos baseados em XML e JSON (baseado em texto) igualmente.

Os leitores mais curiosos e observadores verão que nos métodos fromXml de Status e seu User aninhado estou utilizando a abordagem de decomposição no estilo XPath, em vez de passar por todos os elementos aninhados como sugeri anteriormente. Até agora, a abordagem no estilo XPath parece fácil, mas felizmente, se eu mudar de ideia depois, o bom encapsulamento ole continua meu amigo — posso mudar de ideia mais tarde sem que ninguém fora do Scitter fique sabendo ou se importe.

Observe como dois membros dentro de Status utilizam o tipo Option[T]; isso acontece porque esses elementos são sempre deixados de fora da mensagem de Status, e embora os elementos apareçam, eles aparecem vazios (como em <in_reply_to_user_id></in_reply_to_user_id>). Isso é exatamente o que Option[T] teve que representar, por isso, quando vazios, "Nenhum" é o valor. (Isso significa que para compatibilidade baseada em Java, isso será um pouco mais difícil de acessar; você deverá chamar get() na instância Option resultante, o que não é tão ruim e lida muito bem com o problema "nulls-or-0" que poderia ter surgido.)

Agora, consumir a linha de tempo pública se torna apenas uma questão de:

Listagem 14. Decifrando a Linha de Tempo Pública
    @Test def simplePublicFeedPullAndDeserialize =
    {
      val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml"

      // HttpClient API 101
      val client = new HttpClient()
      val method = new GetMethod(publicFeedURL)
      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))
      val statusCode = client.executeMethod(method)
      val responseBody = new String(method.getResponseBody())

      val responseXML = scala.xml.XML.loadString(responseBody)
      val statuses = responseXML \\ "status"

      for (n <- statuses.elements)
      {
        val s = Status.fromXml(n)
        System.out.println("\t'@" + s.user.screenName + "' wrote " + s.text)
      }
    }

... o que, francamente, parece muito mais claro e fácil de usar.

Juntar tudo isso no singleton do Scitter é apenas uma questão de fazer a consulta, analisar os elementos Status individuais e anexá-los a uma instância List[Status] para retorno, como mostra a Listagem 15:

Listagem 15. Scitter.publicTimeline
package com.tedneward.scitter
{
  import org.apache.commons.httpclient._, auth._, methods._, params._
  import scala.xml._

  object Scitter
  {
    // ...

    /**
     * Consulte a linha de tempo pública para os status mais recentes.
     *
     * Documentações do Twitter dizem:
     * public_timeline
     * Retorna os 20 status mais recentes de usuários não protegidos que configuraram
     * um ícone de usuário customizado. Não requer autenticação.  Observe que
     * a linha de tempo pública fica armazenada em cache por 60 segundos, portanto,
     * solicitá-la com mais frequência é um desperdício de recursos.
     * URL: http://twitter.com/statuses/public_timeline.format
     * Formatos: xml, json, rss, atom
     * Método(s): GET
     * Limite de API: Não aplicável
     * Retorna: lista de elementos de status
     */
    def publicTimeline : List[Status] =
    {
      import scala.collection.mutable.ListBuffer

      val client = new HttpClient()

      val method =
          new GetMethod("http://twitter.com/statuses/public_timeline.xml")

      method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
        new DefaultHttpMethodRetryHandler(3, false))

      client.executeMethod(method)

      val statusLine = method.getStatusLine()
      if (statusLine.getStatusCode() == 200)
      {
        val responseXML =
          XML.loadString(method.getResponseBodyAsString())

        val statusListBuffer = new ListBuffer[Status]

        for (n <- (responseXML \\ "status").elements)
          statusListBuffer += (Status.fromXml(n))

        statusListBuffer.toList
      }
      else
      {
        Nil
      }
    }
  }
}

Nada mau para um trabalho de uma hora. É óbvio que ainda temos uma longa distância para percorrer antes de termos um cliente Twitter totalmente funcional, mas por enquanto, os comportamentos básicos parecem bons.

Conclusão

Nos saímos bem com a construção da biblioteca do Scitter; por enquanto, as coisas parecem simples e claras vistas do lado de fora, como ficou evidenciado pelas implementações de testes do Scitter relativamente fáceis, principalmente em comparação com os testes de exploração que geraram a API do Scitter em si. Os usuários externos não precisam se preocupar com a complexidade da API do Twitter ou seus vários formatos; e embora neste momento a biblioteca do Scitter esteja um pouco sensível para ser submetida a um teste (dependendo da rede, isso não é uma boa ideia para testes de unidade), vamos corrigir isso com o tempo.

Observe que mantive deliberadamente a ideia de orientada a objetos para a API do Twitter, mantendo o espírito do Scala — o fato de o Scala suportar inúmeros recursos funcionais não significa que temos que desacreditar as abordagens de design de objetos adotadas pela estrutura Java. Nós vamos utilizar os recursos funcionais onde fizer sentido, mas manteremos os "modos antigos" ativos onde for adequado.

O que não quer dizer que o design que estou apresentando aqui é a melhor maneira de se abordar o problema, mas sim que esse foi o design que decidi utilizar; e como eu estou escrevendo esse artigo, vamos fazer do meu jeito. Se você não gostar, escreva sua própria biblioteca e seu próprio artigo (e envie-me a URL para que eu possa fazer um agradecimento a você em uma coluna futura). De fato, em um artigo futuro, vamos juntar tudo isso em um pacote "sbaz" do Scala que disponibilizaremos em algum lugar na Web para fácil download.

Agora é hora de cada um seguir seu caminho. No próximo mês vamos incluir mais alguns recursos interessantes na biblioteca do Scitter e começaremos a pensar nas maneiras de facilitar seu teste e uso.

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Software livre
ArticleID=396971
ArticleTitle=O Guia do Desenvolvedor Java Ocupado para o Scala: Scala + Twitter = Scitter
publish-date=05052009