Por que eu devo ler este artigo: Este artigo mostra, de forma prática, as principais operações oferecidas pela API de Contatos Google. Aqui são apresentados os elementos envolvidos no que diz respeito ao gerenciamento de contatos e grupos de contatos de um usuário, bem como as operações realizadas utilizando esses elementos. Para mostrar essas funcionalidades criamos uma aplicação Java Swing, utilizando a Java Client Library.

Veja como gerenciar seus contatos e grupos, editar a imagem dos contatos, bem como adicionar, aos contatos e grupos, atributos específicos à sua aplicação

Com o uso das APIs de Dados Google, de um forma geral, torna-se possível integrar nossas aplicações com vários dos serviços disponibilizados pelo Google. No caso da API de Contatos, nossa aplicação torna-se capaz de recuperar os contatos e grupos de um usuário, bem como criar, remover e alterar esses contatos e grupos.

Nos dias de hoje, a integração entre sistemas está tornando-se quase uma obrigação. Existem várias frentes de trabalho que têm se esforçado no sentido de criar padronizações e meios para facilitar, cada vez mais, essa integração. O Google, diante desse cenário, oferece diversas facilidades, dentre as quais estão as APIs de Dados. Com a API de contatos, no nosso caso, podemos integrar facilmente nossas aplicações com o serviço de contatos e grupos do Google. Isso nos permite realizar facilmente tarefas como a sincronização dos contatos de uma aplicação desktop com nossos contatos Google e assim por diante.

Google Data APIs: Contacts

A API de Contatos Google faz parte das diversas APIs de Dados disponíveis. Com ela é possível gerenciar contatos e grupos de um usuário. Também é possível definir novas propriedades para os contatos. No artigo Google Data APIs vimos que as APIs de Dados trabalham sobre o GData Protocol, que foi criado especialmente para essas APIs, e que os dados são trocados com o servidor no formato XML. Também foi visto que o Google disponibiliza bibliotecas cliente em várias linguagens, incluindo Java. Essas bibliotecas fornecem uma camada de abstração sobre o protocolo GData. No caso da biblioteca Java, por exemplo, cada serviço possui uma classe correspondente. Para representar o serviço de contatos temos a classe ContactsService.

Para inserir um contato, basta passar um ContactEntry, devidamente populado, para o método ContactsService.insert(), juntamente com a URL do feed de contatos. Já para editar um contato existente, o que temos que fazer é recuperar a entrada a ser atualizada, editar o objeto ContactEntry e passá-lo para o método ContactsService.update(). Para remover um contato, chame o método ContactsService.delete(), passando o endereço de edição. Para listar os contatos, é possível usar o método ContactsService.getFeed() ou ContactsService.query(). Ambos retornam um ContactsFeed, o qual possui um método getEntries() que retorna as entradas recuperadas. Estas e outras operações serão expostas neste artigo, assim como as operações envolvendo grupos, que são bastante similares às apresentadas. A diferença básica é que são utilizados os objetos ContactGroupEntry e ContactGroupFeed.

No artigo anterior, tivemos uma visão geral das APIs de Dados Google e vimos o funcionamento básico do protocolo utilizado, o Gdata Protocol, que é a base para as APIs de cada serviço específico. Também foram apresentadas a biblioteca cliente Java (Java Client Library), usada para abstrair a camada do protocolo, e o plugin para Eclipse, além de um exemplo prático utilizando a API do Picasa Web Albums. Ainda no artigo anterior, vimos uma lista com as APIs de dados disponíveis até o momento, dentre as quais está a Google Contact Data API (API de Dados de Contatos Google), a qual será discutida neste artigo.

A API de Contatos nos provê – além dos tradicionais métodos para criação, edição, remoção e listagem / busca de contatos e grupos de contatos – métodos para upload e download da imagem do contato e suporte a atributos estendidos específicos de uma determinada aplicação cliente. Neste artigo veremos como efetuar cada uma dessas operações, tanto no nível de protocolo quanto com exemplos práticos utilizando a Java Client Library. Para que possamos testar as APIs de dados de um modo geral, construiremos um Organizador Pessoal, que será uma aplicação Swing. Como este artigo trata do gerenciamento de contatos e grupos, esta será a primeira feature da nossa aplicação exemplo. Agora vamos deixar a conversa de lado e pôr a mão na massa.

Google Contacts Data API

A API de Contatos Google disponibiliza métodos para que uma aplicação terceira possa acessar e gerenciar os dados referentes aos contatos e grupos de contatos de um determinado usuário. Como vimos no artigo anterior, o protocolo GData é baseado principalmente no Atom, que por sua vez tem como principais elementos o Feed e o Entry. Vimos também que o conteúdo do elemento Entry pode variar de acordo com o tipo de entrada que desejamos representar.

Agora veremos com mais detalhes as entradas que representam contatos e grupos. A Tabela 1 mostra[1] os principais elementos que representam uma Entrada de Contato[2]. Note que os elementos padrão do Atom possuem o namespace atom precedendo seus nomes, enquanto os elementos específicos da API possuem os devidos namespaces (gd e gContact). Note que as multiplicidades dos elementos são representadas pelos símbolos “*” e “?”[3], seguindo o mesmo padrão utilizado em expressões regulares, DTD, etc. O símbolo “?” após o nome do elemento indica que este pode aparecer “zero ou uma” vez(es), enquanto o símbolo “*” indica que o elemento pode aparecer “zero ou muitas” vezes. Caso nenhum símbolo esteja presente ao final do nome do elemento, este é obrigatório e deve aparecer uma única vez.

Elementos de uma entrada que representa um Contato
Tabela 1. Elementos de uma entrada que representa um Contato

A Listagem 1 mostra um exemplo de XML representando um contato. Em contrapartida, a Java Client Library nos fornece classes correspondentes a cada elemento deste XML. Por exemplo, a classe com.google.gdata.data.contacts.ContactEntry representa uma Entrada de Contato. Veremos essa classe com mais detalhes nos exemplos que iremos analisar.


<atom:entry xmlns:atom='http://www.w3.org/2005/Atom'>

<atom:id>

  http://www.google.com/m8/feeds/contacts/pcmnac.jm%40gmail
  .com/base/2ab854f0b592dda

</atom:id>

<atom:updated>2008-11-05T23:29:04.998Z</atom:updated>

<atom:category scheme='http://schemas.google.com/g/2005#kind'

    term='http://schemas.google.com/contact/2008#contact'/>

<atom:title type='text'>João da Silva</atom:title>

<atom:content type='text'>Observações sobre esse 
contato</atom:content>

<atom:link rel='http://schemas.google.com/contacts/2008/rel#edit-photo' 
type='image/*'

    href='http://www.google.com/m8/feeds/photos/media/pcmnac
    .jm%40gmail.com/2ab854f0b592dda/tGg'/>

<atom:link rel='http://schemas.google.com/contacts/2008/rel#photo' 
type='image/*'

    href='http://www.google.com/m8/feeds/photos/media/pcmnac
    .jm%40gmail.com/2ab854f0b592dda'/>

<atom:link rel='self' type='application/atom+xml'

    href='http://www.google.com/m8/feeds/contacts/pcmnac
    .jm%40gmail.com/full/2ab854f0b592dda'/>

<atom:link rel='edit' type='application/atom+xml'

    href='http://www.google.com/m8/feeds/contacts/pcmnac
    .jm%40gmail.com/full/2ab854f0b592dda/12'/>

<gd:organization xmlns:gd='http://schemas.google.com/g/2005' 
primary='false'

    rel='http://schemas.google.com/g/2005#other'>

  <gd:orgName>Empresa A</gd:orgName>

  <gd:orgTitle>Analista</gd:orgTitle>

</gd:organization>

<gd:organization xmlns:gd='http://schemas.google.com/g/2005'
primary='true'

    rel='http://schemas.google.com/g/2005#work'>

  <gd:orgName>Empresa B</gd:orgName>

  <gd:orgTitle>Diretor</gd:orgTitle>

</gd:organization>

<gd:email xmlns:gd='http://schemas.google.com/g/2005'

    rel='http://schemas.google.com/g/2005#home' 
    address='contato1@gmail.com' primary='true'/>

<gd:im xmlns:gd='http://schemas.google.com/g/2005' 
address='contato1@msn.com'

    label='CUSTOM' primary='false' 
    protocol='http://schemas.google.com/g/2005#MSN'/>

<gd:phoneNumber xmlns:gd='http://schemas.google.com/g/2005'

    rel='http://schemas.google.com/g/2005#mobile'>

  (66) 7777-8888

</gd:phoneNumber>

<gd:phoneNumber xmlns:gd='http://schemas.google.com/g/2005'

    rel='http://schemas.google.com/g/2005#home'>

  (12) 1234-2134

</gd:phoneNumber>

<gd:postalAddress xmlns:gd='http://schemas.google.com/g/2005'

    rel='http://schemas.google.com/g/2005#home'>

  Rua João e Maria N°1000, Recife-PE

</gd:postalAddress>

<gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005'

  name='Job' value='STUDENT'/>

</atom:entry>
Listagem 1. Exemplo de uma Entrada de Contato

Vamos agora analisar os elementos que representam uma Entrada de Grupo de contatos. A Tabela 2 nos mostra esses elementos e a Listagem 2 mostra um exemplo de XML. Assim como no caso da Entrada de Contato, a Java Client Library possui uma classe para representar os dados de um grupo, a classe com.google.gdata.data.contacts.ContactGroupEntry.

Elemento Descrição

atom:id? Identificador único do grupo. Criado pelo servidor.
atom:updated? Indica o tipo de entrada, Grupo nesse caso. Possui o valor: <atom:category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/g/2008#group"/>
atom:title Nome do grupo.
atom:content Nome do grupo. Não pode ser modificado diretamente. As alterações feitas no elemento atom:title são automaticamente refletidas neste elemento.
atom:link* Links para informações relacionadas ao Grupo.
gd:extendedProperty* Propriedades estendidas (específicas de uma determinada aplicação) do grupo.
gd:deleted? Se presente, indica que o grupo foi deletado.
gContact:systemGroup? Identificador para diferenciar os grupos do sistema.
Tabela 2. Elementos de uma entrada que representa um Grupo

<atom:entry xmlns:atom='http://www.w3.org/2005/Atom'>

  <atom:id>

    http://www.google.com/m8/feeds/groups/pcmnac.jm%40gmail.com/base/52e9cac309780704

  </atom:id>

  <atom:updated>2008-11-06T00:04:09.630Z</atom:updated>

  <atom:category scheme='http://schemas.google.com/g/2005#kind'

      term='http://schemas.google.com/contact/2008#group'/>

  <atom:title type='text'>Família</atom:title>

  <atom:content type='text'>Família</atom:content>

  <atom:link rel='self' type='application/atom+xml'

      href='http://www.google.com/m8/feeds/groups/pcmnac
      .jm%40gmail.com/full/52e9cac309780704'/>

  <atom:link rel='edit' type='application/atom+xml'

      href='http://www.google.com/m8/feeds/groups/pcmnac
      .jm%40gmail.com/full/52e9cac309780704/122'/>

  <gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005'

      name='Description' value='descrição qualquer'/>

</atom:entry>
Listagem 2. Exemplo de uma Entrada de Grupo

De um modo geral, para poder recuperar as informações de um feed ou de uma entrada (entry) específica, é necessário conhecer as URLs dos recursos com os quais estamos lidando. Nas APIs de dados Google, as URLs utilizadas para recuperar os feeds de contatos seguem o seguinte formato:


http://www.google.com/m8/feeds/contacts/userId/projection

Onde userId representa o e-mail[4] do usuário do qual os contatos devem ser recuperados e projection indica a forma como os dados devem ser recuperados (a seguir veremos as opções disponíveis). No caso dos feeds de grupos as URLs seguem o formato:


http://www.google.com/m8/feeds/groups/userId/projection

Os valores de projeção permitidos na API de Contatos são os seguintes:

  • thin – Nenhuma propriedade estendida (elemento gd:extendedProperty) é retornada ou atualizada;
  • property-KEYKEY deve ser substituído pelo valor da chave de uma propriedade que deve ser incluída nas pesquisas e atualizações. Se você deseja que seja incluída uma propriedade de nome Job, por exemplo, use property-Job como valor de projeção. Se essa propriedade não estiver presente numa operação de atualização, esta será removida no servidor;
  • full – Todas as propriedades estendidas são retornadas nas pesquisas e incluídas nas atualizações. Se alguma propriedade estendida existente não estiver presente numa operação de atualização, esta será removida no servidor.

Assim como foi visto no artigo anterior, podemos usar queries para recuperar os dados de acordo com os critérios que definirmos. A Tabela 3 mostra as opções de parâmetros que podem[5] ser utilizados numa query para listar contatos. Os parâmetros marcados com “*” são específicos[6] da API de Contatos. Todos os parâmetros da Tabela 3, exceto group, também são válidos para grupos.

Parâmetro Descrição

alt Usado para alterar o formato da resposta. Os valores válidos são: atom (o padrão), rss e json.
max-results Número máximo de entradas a serem retornadas. Para recuperar todos os contatos, use um valor grande o suficiente (atualmente o valor padrão é 25).
start-index Índice (iniciando em 1) do primeiro registro a ser retornado. Utilizado para paginação.
updated-min Seleciona apenas os contatos que foram atualizados após a data informada.
orderby* Inclui os contatos deletados no feed. Quando um contato é deletado, sua entrada permanece no servidor por 30 dias. Os valores válidos são true e false.
showdeleted* Inclui os contatos deletados no feed. Quando um contato é deletado, sua entrada permanece no servidor por 30 dias. Os valores válidos são true e false.
sortorder* Especifica a direção da ordenação. Os valores aceitos são ascending e descending.
group* Mostra apenas os contatos do grupo indicado.
Tabela 3. Parâmetros de query disponíveis para pesquisar Contatos e Grupos

Aplicação exemplo (Organizador Pessoal)

Agora que vimos as estruturas básicas dos objetos que lidaremos, bem como as diversas opções de projeção e parâmetros de query que podem ser utilizados para refinar esses objetos, veremos como realizar as principais operações disponibilizadas pela API de Contatos Google. Para isso construiremos uma pequena aplicação Swing, um Organizador Pessoal, que inicialmente irá manipular apenas os contatos e grupos de um usuário. (Iremos incrementar a mesma aplicação nos próximos artigos, à medida que formos explorando as demais APIs de dados.)

Primeiramente precisaremos baixar, caso já não o tenhamos feito, a Java Client Library e suas dependências. Os links para download encontram-se na seção Links.

Iniciaremos criando os beans, classes de infra-estrutura e esqueleto da nossa classe de negócio, em seguida acrescentaremos os métodos responsáveis por cada operação a esta classe e, por fim, criaremos uma pequena interface gráfica para testar as funcionalidades. As Listagens 3 e 4 mostram, respectivamente, os beans Contact e Group. Poderíamos usar diretamente as classes ContactEntry e GroupEntry, ambas disponíveis na Java Client Library, mas em geral é melhor termos nossas próprias classes de domínio, pois conseguimos ter uma certa independência da API, além de que, num projeto real, são poucas as chances dessas classes atenderem aos requisitos.

Na classe Contact (Listagem 3), primeiramente temos a definição do enum Job contendo uma pequena lista de ocupações. Temos em seguida a declaração dos atributos da classe incluindo o atributo job do tipo Job, e o atributo groups, contendo uma lista de objetos do tipo Group, representando os grupos dos quais o contato faz parte.

Perceba que o atributo Job não faz parte dos atributos padrão de uma Entrada de Contato. A nossa intenção com esse atributo é mostrar o uso de atributos estendidos. Como vimos anteriormente, um atributo não padrão pode ser mapeado usando o elemento extendedProperty. A Java Client Library possui uma classe ExtendedProperty para este fim. No método setPhoto() copiamos os bytes da foto do contato para o atributo da classe. Por fim sobrescrevemos o método toString() para retornar o nome do contato; isso será útil para montarmos a interface gráfica mais adiante.

A classe Group é ainda mais simples (ver Listagem 4). Iniciamos com as declarações dos atributos do grupo. O atributo description também não é um elemento padrão de uma Entrada de Grupo da API e deve ser mapeado como uma ExtendedProperty. Depois sobrescrevemos o método toString(), assim como no caso anterior e, por fim, sobrescrevemos também o método equals(), para que possamos identificar um grupo pelo seu ID.


// declaração de pacote e imports omitidos.

public class Contact {

  public enum Job {

    OTHER, STUDENT, TRAINEE, DESIGNER, ARCHITECT, ENGINEER, DOCTOR

  }

  private String id;

  private String name;

  private String email;

  private String phoneNumber;

  private String mobileNumber;

  private Job job = Job.OTHER;

  private byte[] photo;

  private List<Group> groups = new ArrayList<Group>();

 

  // gets e sets omitidos...

 

  public void setPhoto(byte[] photo) {

    if (photo != null) {

      this.photo = new byte[photo.length];

      System.arraycopy(photo, 0, this.photo, 0, photo.length);

    } else {

      this.photo = null;

    }

  }

 

  public String toString() {

    return name;

  }

}
Listagem 3. Classe de bean Contact. Representa um Contato na nossa aplicação

// declaração de pacote e imports omitidos.

public class Group {

 

  String id;

  String title;

  String description;

 

  public String toString() { return title; }

 

  public boolean equals(Object obj) {

    return (obj instanceof Group) && (id != null) && (id.equals(((Group) obj).getId()));

  }

 

}
Listagem 4. Classe de bean Group. Representa um Grupo de Contatos na nossa aplicação

Com nossas classes de domínio criadas, vamos criar as classes que fornecerão a infra-estrutura da nossa aplicação e a classe de negócio, responsável por interagir com a API de Contatos Google através dos recursos da Java Client Library.

A Listagem 5 mostra o código da classe CredentialsManager, responsável por armazenar as credenciais do usuário. Essa classe é um singleton simples e contém apenas dois atributos, email e password, com seus métodos get. O método setCredentials() é utilizado para configurar as credenciais do usuário. A Listagem 6, por sua vez, mostra a interface Constants que contém as constantes que utilizaremos na aplicação.

A Listagem 7 mostra a definição da classe abstrata AbstractServiceManager, que irá conter o comportamento comum a todas as classes responsáveis por gerenciar algum tipo de serviço. Primeiro temos a declaração do atributo service, do tipo GoogleService. Essa é a classe base das classes de serviço específicas, como ContactsService, BloggerService, CalendarService, etc. A seguir temos o atributo authenicated, que servirá para indicar se o usuário já está autenticado num determinado serviço ou não. O método authenticate() é responsável por autenticar o usuário no serviço em questão, se este ainda não estiver autenticado. Note que estamos usando a autenticação do tipo ClientLogin[7], que é indicado para aplicações desktop. No construtor, apenas efetuamos uma chamada ao método reset(), que por sua vez, “seta” para false o atributo authenticated e cria o objeto do serviço através do método abstrato createService(). Este método deve ser implementado pelas subclasses responsáveis por cada serviço específico.


// declaração de pacote e imports omitidos.

public class CredentialsManager {

 

  public static CredentialsManager getInstance() { return INSTANCE; }

  public String getEmail() { return email; }

  public String getPassword() { return password; }

 

  public void setCredentials(String email, String password) {

    this.email = email;

    this.password = password;

  }

 

  private static final CredentialsManager INSTANCE = new CredentialsManager();

  private String email;

  private String password;

 

  private CredentialsManager() {}

}
Listagem 5. Classe CredentialsManager. Responsável por armazenar as credenciais do usuário

// declaração de pacote e imports omitidos.

public interface Constants {

  String APPLICATION_NAME = "JavaMagazine-GData-PersonalOrganizer";

  String EXTENDED_PROPERTY_JOB = "Job";

  String EXTENDED_PROPERTY_DESCRIPTION = "Description";

  String CUSTOM_PARAMETER_GROUP = "group";

}
Listagem 6. Interface Constants. Define as constantes globais utilizadas na aplicação

// declaração de pacote e imports omitidos.

public abstract class AbstractServiceManager {

 

  private GoogleService service;

  private boolean authenticated;

 

  protected void authenticate() {

    if (!authenticated) {

      CredentialsManager cm = CredentialsManager.getInstance();

      try {

        service.setUserCredentials(cm.getEmail(), cm.getPassword());

        authenticated = true;

      } catch (AuthenticationException e) { e.printStackTrace(); }

    }

  }

 

  public AbstractServiceManager () { reset(); }

 

  public void reset() {

    authenticated = false;

    service = createService();

  }

 

  protected abstract GoogleService createService();

}
Listagem 7. Classe AbstractServiceManager. Base para os demais gerenciadores de serviços

Com nossa infra-estrutura de classes criadas, vamos criar a estrutura inicial da classe ContactsServiceManager, que herdará de AbstractServiceManager e será responsável pela interação entre nossa aplicação e o serviço de contatos Google através da Google Contacts Data API. O código dessa classe pode ser visto na Listagem 8. Primeiramente temos o atributo estático INSTANCE que guarda a única instância dessa classe (padrão singleton). Logo em seguida temos o atributo service do tipo ContactsService, que é uma subclasse de GoogleService especializada para trabalhar com Entradas de Contatos e Entradas de Grupos. Temos a implementação do método createService(), definido como abstrato na classe AbstractServiceManager. Nossa implementação simplesmente cria um novo objeto ContactsService, armazena uma referência para esse objeto no atributo service e o retorna.

O método parseContact() é responsável por converter um ContactEntry no nosso bean Contact. Primeiro criamos uma nova instância de Contact e configuramos seus ID e Name. Perceba que o método getTitle() da classe BaseEntry (base de ContactEntry e demais tipos de entrada específicos) retorna um TextConstruct, por isso precisamos chamar o método getPlainText() para recuperar o valor String do nome do contato. Em seguida, iteramos sobre os e-mails e telefones presentes no objeto entry e através do seu atributo rel descobrimos o tipo; estamos recuperando apenas o e-mail cujo tipo de relação é igual a Email.Rel.OTHER. Usamos os tipos de relação PhoneNumber.Rel.HOME e vPhoneNumber.Rel.MOBILE para identificar o telefone convencional e o celular do contato. Para cada GroupMembershipInfo presente na entrada, criamos um novo Group e o adicionamos ao contato. Por fim procuramos o objeto ExtendedProperty cujo nome é igual ao definido na constante Constants.EXTENDED_PROPERTY_JOB e convertemos seu valor para uma das opções do enum Job.

O método buildContactEntry() faz o trabalho inverso, ele pega os dados de um objeto Contact e preenche um ContactEntry com esses dados. A parte menos trivial é a configuração do e-mail, telefone e celular na entrada. Primeiro verificamos se a entrada já possui algum desses atributos com o tipo de relação apropriado. Caso a entrada já os possua (um e-mail cujo tipo de relação é OTHER, por exemplo), o valor será sobrescrito. Caso contrário, um novo objeto do tipo apropriado é criado, configurado e adicionado à entrada.

Os métodos parseGroup() e buildGroupEntry() são semelhantes a parseContact() e buildContactEntry(), respectivamente, mas lidam com grupos ao invés de contatos. Por último temos o método getInstance() usado para recuperar a instancia única de ContactsServiceManager.

Criamos a estrutura básica da nossa classe gerenciadora de contatos e grupos, mas ainda não vimos como interagir com o servidor. A partir de agora veremos como realizar cada uma das principais operações providas pela API de Contatos Google.


// declaração de pacote e imports omitidos.

public class ContactsServiceManager extends AbstractServiceManager {

 

  private static final ContactsServiceManager INSTANCE = new ContactsServiceManager();

  private ContactsService service;

 

  protected GoogleService createService() {

    service = new ContactsService(Constants.APPLICATION_NAME);

    return service;

  }

 

  private Contact parseContact(ContactEntry entry) {

    Contact contact = new Contact();

    contact.setId(entry.getId());

    contact.setName(entry.getTitle().getPlainText());

 

    for (Email email : entry.getEmailAddresses()) {

      if (email.getRel().equals(Email.Rel.OTHER)) {

        contact.setEmail(email.getAddress());

        break;

      }

    }

    for (PhoneNumber phoneNumber : entry.getPhoneNumbers()) {

      if (phoneNumber.getRel().equals(PhoneNumber.Rel.HOME)) {

        contact.setPhoneNumber(phoneNumber.getPhoneNumber());

      }

      if (phoneNumber.getRel().equals(PhoneNumber.Rel.MOBILE)) {

        contact.setMobileNumber(phoneNumber.getPhoneNumber());

      }

    }

    for (GroupMembershipInfo groupMembershipInfo : entry.getGroupMembershipInfos()) {

      Group group = new Group();

      group.setId(groupMembershipInfo.getHref());

      contact.getGroups().add(group);

    }

    for (ExtendedProperty property : entry.getExtendedProperties()) {

      if (property.getName().equals(Constants.EXTENDED_PROPERTY_JOB)) {

        contact.setJob(Enum.valueOf(Contact.Job.class, property.getValue()));

        break;

      }

    }

    return contact;

  }

 

  private void buildContactEntry(ContactEntry entry, Contact contact) {

 

    entry.setTitle(new PlainTextConstruct(contact.getName()));

 

    boolean existingEmail = false;

    for (Email email : entry.getEmailAddresses()) {

      if (email.getRel().equals(Email.Rel.OTHER)) {

        existingEmail = true;

        if ((contact.getEmail() != null) && (!contact.getEmail().equals(""))) {

          email.setAddress(contact.getEmail());

        } else { entry.getEmailAddresses().remove(email); }

        break;

      }

    }

    if ((!existingEmail) && (contact.getEmail() != null) && (!contact.getEmail().equals(""))) {

      Email email = new Email();

      email.setRel(Email.Rel.OTHER);

      email.setAddress(contact.getEmail());

      entry.addEmailAddress(email);

    }

    boolean existingPhone = false;

    for (PhoneNumber phoneNumber : entry.getPhoneNumbers()) {

      if (phoneNumber.getRel().equals(PhoneNumber.Rel.HOME)) {

        existingPhone = true;

        if ((contact.getPhoneNumber() != null)

            && (!contact.getPhoneNumber().equals(""))) {

          phoneNumber.setPhoneNumber(contact.getPhoneNumber());

        } else { entry.getPhoneNumbers().remove(phoneNumber); }

        break;

      }

    }

    if ((!existingPhone) && (contact.getPhoneNumber() != null)

        && (!contact.getPhoneNumber().equals(""))) {

      PhoneNumber phoneNumber = new PhoneNumber();

      phoneNumber.setRel(PhoneNumber.Rel.HOME);

      phoneNumber.setPhoneNumber(contact.getPhoneNumber());

      entry.addPhoneNumber(phoneNumber);

    }

    boolean existingMobile = false;

    for (PhoneNumber phoneNumber : entry.getPhoneNumbers()) {

      if (phoneNumber.getRel().equals(PhoneNumber.Rel.MOBILE)) {

        existingMobile = true;

        if ((contact.getMobileNumber() != null)

            && (!contact.getMobileNumber().equals(""))) {

          phoneNumber.setPhoneNumber(contact.getMobileNumber());

        } else { entry.getPhoneNumbers().remove(phoneNumber); }

        break;

      }

    }

    if ((!existingMobile) && (contact.getMobileNumber() != null)

        && (!contact.getMobileNumber().equals(""))) {

      PhoneNumber mobileNumber = new PhoneNumber();

      mobileNumber.setRel(PhoneNumber.Rel.MOBILE);

      mobileNumber.setPhoneNumber(contact.getMobileNumber());

      entry.addPhoneNumber(mobileNumber);

    }

    for (Group group : contact.getGroups()) {

      GroupMembershipInfo groupMembershipInfo = new GroupMembershipInfo();

      groupMembershipInfo.setHref(group.getId());

      entry.addGroupMembershipInfo(groupMembershipInfo);

    }

    if (contact.getJob() != null) {

      ExtendedProperty job = new ExtendedProperty();

      job.setName(Constants.EXTENDED_PROPERTY_JOB);

      job.setValue(contact.getJob().name());

      entry.addExtendedProperty(job);

    }

  }

 

  private Group parseGroup(ContactGroupEntry entry) {

    Group group = new Group();

    group.setId(entry.getId());

    group.setTitle(entry.getTitle().getPlainText());

 

    for (ExtendedProperty property : entry.getExtendedProperties()) {

      if (property.getName().equals(Constants.EXTENDED_PROPERTY_DESCRIPTION)) {

        group.setDescription(property.getValue());

      }

    }

 

    return group;

  }

 

  private void buildGroupEntry(ContactGroupEntry entry, Group group) {

    if ((group.getTitle() != null) && (!group.getTitle().equals(""))) {

      entry.setTitle(new PlainTextConstruct(group.getTitle()));

    }

    if ((group.getDescription() != null) && (!group.getDescription().equals(""))) {

      ExtendedProperty description = new ExtendedProperty();

      description.setName(Constants.EXTENDED_PROPERTY_DESCRIPTION);

      description.setValue(group.getDescription());

      entry.addExtendedProperty(description);

    }

  }

 

  public static ContactsServiceManager getInstance() {

    return INSTANCE;

  }

 

}
Listagem 8. Estrutura inicial da classe ContactsServiceManager, responsável por realizar a interação com o serviço de contatos

Inserindo um novo contato com foto

No artigo anterior, vimos que para inserir uma nova entrada basta enviar um HTTP POST para o endereço do feed, colocando a representação XML da nova entrada no corpo da requisição. Sendo assim, para inserir um novo contato teríamos uma requisição parecida com a seguinte:


POST /m8/feeds/contacts/usuarioteste%40gmail.com/full

 

<?xml version="1.0"?>

<atom:entry xmlns="http://www.w3.org/2005/Atom">

  <atom:category scheme='http://schemas.google.com/g/2005#kind' 
  term='http://schemas.google.com/contact/2008#contact'/>

  ...

</atom:entry>

Não é possível configurar a foto do contato no momento da criação deste. Para adicionar ou editar a foto de um contato é preciso uma requisição adicional. Para tal, basta enviar um HTTP PUT, com os bytes da foto no corpo da requisição, para o link de edição de foto, criado automaticamente pelo servidor quando um novo contato é criado.

Vejamos então como fazer isso usando a Java Client Library. A Listagem 9 mostra o método createContact() que adicionaremos à classe ContactsServiceManager. Iniciamos chamando o método authenticate(), visto anteriormente, para realizar a autenticação do usuário no serviço de contatos. Criamos um novo ContactEntry e o preenchemos com os dados do contato recebido através do método buildContactEntry(). Em seguida criamos a URL do feed de contatos do usuário e chamamos o método insert() do objeto service. Este método trata todas as questões do protocolo relacionadas à inserção de uma Entrada de Contato. Note que o método insert() retorna um objeto ContactEntry, que já contém todos os dados acrescentados pelo servidor no momento da criação.

Então se o contato possuir uma foto, enviamos seus bytes para o servidor. Para isso recuperamos o link de edição de foto através do método getContactEditPhotoLink() de ContactEntry, criando uma URL com este link; criamos um objeto GDataRequest chamando o método createRequest() no objeto service, informando o tipo de requisição (UPDATE no caso), a URL e o tipo de conteúdo (em nosso caso, “image/jpeg”); escrevemos os bytes da foto no OutputStream da requisição e, finalmente, executamos a requisição chamando o método execute(). Por último, a entrada atualizada é convertida num objeto Contact através do método parseContact() e esse objeto é retornado.


// declaração de pacote e imports omitidos.

public class ContactsServiceManager extends AbstractServiceManager {

  ...

 

  public Contact createContact(Contact contact) {

    Contact result = contact;

 

    try {

      authenticate();

      ContactEntry entry = new ContactEntry();

      buildContactEntry(entry, contact);

      URL feedUrl = new URL("http://www.google.com/m8/feeds/contacts/"

          + CredentialsManager.getInstance().getEmail() + "/full");

      entry = service.insert(feedUrl, entry);

 

      if (contact.getPhoto() != null) {

        Link photoEditLink = entry.getContactEditPhotoLink();

        URL photoEditUrl = new URL(photoEditLink.getHref());

        GDataRequest request = service.createRequest(

            GDataRequest.RequestType.UPDATE, photoEditUrl, 
            new ContentType("image/jpeg"));

        OutputStream out = request.getRequestStream();

        out.write(contact.getPhoto());

        request.execute();

      }

      result = parseContact(entry);

    } catch (Exception e) { e.printStackTrace(); }

    return result;

  }

}
Listagem 9. Método createContact(), responsável por criar um contato no servidor, incluindo sua foto

Atualizando um contato e sua foto

Para atualizar uma Entrada de Contato basta enviar uma requisição HTTP PUT para o endereço de edição da entrada, inserindo a representação XML da entrada atualizada no corpo. Uma requisição de atualização se parece com o seguinte:


PUT /m8/feeds/contacts/usuarioteste%40gmail.com/full/123214/54bvd77

 

<?xml version="1.0"?>

<atom:entry xmlns="http://www.w3.org/2005/Atom">

  <atom:category scheme='http://schemas.google.com/g/2005#kind' 
  term='http://schemas.google.com/contact/2008#contact'/>

  ...

</atom:entry>

Agora vejamos como fazê-lo usando a Java Client Library. Acrescentemos o método editContact() (Listagem 10) à classe ContactsServiceManager. Esse método é semelhante a createContact(), já visto, com algumas diferenças que discutiremos agora. Primeiramente recuperamos a entrada do servidor usando o método getEntry() de service. Note que a URL utilizada para recuperar um contato é seu próprio ID. De posse da entrada, atualizamos seus dados, utilizando o contato recebido como parâmetro, através do método buildContactEntry(). Depois de realizadas as devidas atualizações, chamamos o método update() do objeto service, a fim de atualizar os dados do contato no servidor. Perceba que criamos a URL usando o link de edição da entrada, porém substituindo a projeção base[8] por full. Isto é necessário, pois estamos utilizando atributos estendidos.

Caso não fizéssemos essa substituição, o valor do atributo job nunca seria atualizado. O código para atualização da foto é quase idêntico ao de createContact(), com a diferença de que se o contato recebido tiver seu atributo photo igual a null, entenderemos que o usuário deseja remover a foto para esse contato. Nesse caso, chamamos o método delete() do objeto service. No final, preenchemos o contato com os dados atualizados e o retornamos.


// declaração de pacote e imports omitidos.

public class ContactsServiceManager extends AbstractServiceManager {

  ...

  public Contact editContact(Contact contact) {

    Contact result = contact;

 

    try {

      authenticate();

      URL entryUrl = new URL(contact.getId());

      ContactEntry entry = service.getEntry(entryUrl, ContactEntry.class);

      buildContactEntry(entry, contact);

 

      URL editUrl = new URL(entry.getEditLink().getHref().replace("/base/", "/full/"));

      entry = service.update(editUrl, entry);

 

      Link photoEditLink = entry.getContactEditPhotoLink();

      URL photoEditUrl = new URL(photoEditLink.getHref());

      if (contact.getPhoto() != null) {

        GDataRequest request = service.createRequest(

            GDataRequest.RequestType.UPDATE, photoEditUrl, new ContentType("image/jpeg"));

        OutputStream out = request.getRequestStream();

        out.write(contact.getPhoto());

        request.execute();

      } else {

        if (entry.getContactPhotoLink() != null) { service.delete(photoEditUrl); }

      }

      result = parseContact(entry);

    } catch (Exception e) { e.printStackTrace(); }

    return result;

  }

}
Listagem 10. Método editContact(), responsável por editar um contato e sua foto no servidor

Removendo um contato

Para remover um contato, tudo o que temos de fazer é enviar uma requisição HTTP DELETE para o endereço de edição da entrada, como no exemplo a seguir:


DELETE /m8/feeds/contacts/usuarioteste%40gmail.com/full/123214/54bvd77

Vamos ver como seria um código Java para realizar essa tarefa. A Listagem 11 mostra o método deleteContact(), acrescentado à classe ContactsServiceManager. Nele, iniciamos autenticando o usuário no serviço, recuperamos a entrada correspondente ao objeto Contact recebido como parâmetro, criamos uma URL com o endereço de edição da entrada e, por fim, chamamos o método delete() do objeto service.


// declaração de pacote e imports omitidos.

public class ContactsServiceManager extends AbstractServiceManager {

  ...

  public void deleteContact(Contact contact) {

    try {

      authenticate();

      URL entryUrl = new URL(contact.getId());

      ContactEntry entry = service.getEntry(entryUrl, ContactEntry.class);

      URL editUrl = new URL(entry.getEditLink().getHref());

      service.delete(editUrl);

    } catch (Exception e) { e.printStackTrace(); }

  }

}
Listagem 11. Método deleteContact(), responsável por remover um contato no servidor

Listando os contatos do usuário

Para recuperar a lista de contatos de um usuário basta enviar um HTTP GET para o endereço do feed de contatos desse usuário. Opcionalmente, podemos acrescentar parâmetros[9] à URL do feed para interferir no que será retornado. O exemplo a seguir mostra como seria a URL para recuperar os contatos do usuário “usuarioteste@gmail.com” pertencentes ao grupo “meugrupo”:


GET /m8/feeds/contacts/usuarioteste%40gmail.com/full/?group=meugrupo

Vejamos então como fazer isso no Java. A Listagem 12 mostra o método listContacts(), também adicionado à classe ContactsServiceManager. Primeiro criamos a lista de contatos que será retornada. Na seqüência, montamos a URL do feed de contatos do usuário autenticado. Criamos um objeto Query que conterá os parâmetros adicionais. Configuramos a quantidade máxima de resultados para 10000, para que todos os contatos do usuário possam ser retornados. Se algum grupo foi informado, acrescentamos um filtro por grupo.

Após isso, recuperamos um objeto ContactFeed, contendo todas as entradas de contatos, através do método query()[10] do objeto service. Uma vez com o ContactFeed em mãos, iteramos pelas entradas desse feed, recuperadas através do método getEntries(), convertemos essas entradas em objetos Contact e adicionamos esses objetos à lista que criamos. Por fim, retornamos a lista de contatos. A segunda versão é apenas um método de conveniência para recuperar os contatos de qualquer grupo.


// declaração de pacote e imports omitidos.

public class ContactsServiceManager extends AbstractServiceManager {

  ...

  public List<Contact> listContacts(String groupId) {

    List<Contact> list = new ArrayList<Contact>();

 

    try {

      authenticate();

      URL feedUrl = new URL("http://www.google.com/m8/feeds/contacts/"

          + CredentialsManager.getInstance().getEmail() + "/full");

 

      Query query = new Query(feedUrl);

      query.setMaxResults(10000);

      if ((groupId != null) && (!groupId.equals(""))) {

        query.addCustomParameter(new Query.CustomParameter(

            Constants.CUSTOM_PARAMETER_GROUP, groupId));

      }

      ContactFeed resultFeed = service.query(query, ContactFeed.class);

      for (ContactEntry entry : resultFeed.getEntries()) {

        list.add(parseContact(entry));

      }

    } catch (Exception e) { e.printStackTrace(); }

 

    return list;

  }

 

  public List<Contact> listContacts() { return listContacts(null); }

}
Listagem 12. Método listContacts(), responsável por listar os contatos do usuário

Recuperando os dados de um contato incluindo sua foto

Para recuperar as informações que dizem respeito a um determinado contato, basta enviar uma requisição HTTP GET para a URL que representa o ID do contato. Veja um exemplo:


GET /m8/feeds/contacts/usuarioteste%40gmail.com/full/as7a6s8ddw

A foto do contato não faz parte da Entrada de Contato recuperada pelo exemplo anterior. Para recuperar os bytes que representam a foto de um contato é necessária uma requisição específica para o link da foto do contato. Veja um exemplo:


GET http://google.com/m8/feeds/photos/media/usuarioteste%40gmail.com/c9012de

A Listagem 13 mostra o método getContact() adicionado à classe ContactsServiceManager. Nele, primeiramente autenticamos o usuário, como nos demais métodos, em seguida recuperamos a Entrada de Contato correspondente ao ID recebido. Construímos um Contact a partir da entrada recuperada, verificamos se a entrada possui um link de foto e se esta possuir criamos um InputStream, usando o método getStreamFormLink() do objeto service para recuperar os bytes da foto. Por fim, configuramos a foto no objeto Contact criado e retornamos esse objeto.


// declaração de pacote e imports omitidos.

public class ContactsServiceManager extends AbstractServiceManager {

  ...

  public Contact getContact(String contactId) {

    Contact contact = null;

 

    try {

      authenticate();

      URL entryUrl = new URL(contactId.replace("/base/", "/full/"));

      ContactEntry entry = service.getEntry(entryUrl, ContactEntry.class);

 

      if (entry != null) {

        contact = parseContact(entry);

        Link photoLink = entry.getContactPhotoLink();

        if (photoLink != null) {

          InputStream in = service.getStreamFromLink(photoLink);

          ByteArrayOutputStream out = new ByteArrayOutputStream();

          byte[] buffer = new byte[2048];

          for (int read = 0; (read = in.read(buffer)) != -1;) {

            out.write(buffer, 0, read);

          }

          contact.setPhoto(out.toByteArray());

        }

      }

    } catch (Exception e) { e.printStackTrace(); }

    return contact;

  }

}
Listagem 13. Método getContact(), responsável por recuperar os dados de um contato incluindo sua foto

Gerenciando Grupos de Usuários

Como os métodos para gerenciamento de grupos de contatos são muito parecidos com os de gerenciamento de Contatos, porém mais simples, não explicaremos detalhadamente cada método para não tornar o artigo muito extenso. A Listagem 14 mostra a implementação dos métodos getGroup(), createGroup(), editGroup(), deleteGroup() e listGroups(), todos adicionados à classe ContactsServiceManager. Note que a lógica é bastante parecida com a dos métodos que gerenciam contatos, com a diferença que usam os tipos ContactGroupEntry, ContactGroupFeed e Group.


// declaração de pacote e imports omitidos.

public Group getGroup(String groupId) {

    Group group = null;

    try {

      authenticate();

      URL entryUrl = new URL(groupId.replace("/base/", "/full/"));

      ContactGroupEntry entry = service.getEntry(entryUrl, ContactGroupEntry.class);

      if (entry != null) {

        group = parseGroup(entry);

      }

    } catch (Exception e) { e.printStackTrace(); }

    return group;

  }

 

  public Group createGroup(Group group) {

    Group result = group;

    try {

      authenticate();

      ContactGroupEntry entry = new ContactGroupEntry();

      URL feedUrl = new URL("http://www.google.com/m8/feeds/groups/"

          + CredentialsManager.getInstance().getEmail() + "/full");

      buildGroupEntry(entry, group);

      entry = service.insert(feedUrl, entry);

      result = parseGroup(entry);

    } catch (Exception e) { e.printStackTrace(); }

    return result;

  }

 

  public Group editGroup(Group group) {

    Group result = group;

    try {

      authenticate();

      URL entryUrl = new URL(group.getId());

      ContactGroupEntry entry = service.getEntry(entryUrl, ContactGroupEntry.class);

      URL editUrl = new URL(entry.getEditLink().getHref().replace("/base/", "/full/"));

      buildGroupEntry(entry, group);

      entry = service.update(editUrl, entry);

      result = parseGroup(entry);

    } catch (Exception e) { e.printStackTrace(); }

    return result;

  }

 

  public void deleteGroup(Group group) {

    try {

      authenticate();

      URL entryUrl = new URL(group.getId());

      ContactGroupEntry entry = service.getEntry(entryUrl, ContactGroupEntry.class);

      URL editUrl = new URL(entry.getEditLink().getHref());

      service.delete(editUrl);

    } catch (Exception e) { e.printStackTrace(); }

  }

 

  public List<Group> listGroups() {

    List<Group> list = new ArrayList<Group>();

    try {

      authenticate();

      URL feedUrl = new URL("http://www.google.com/m8/feeds/groups/"

          + CredentialsManager.getInstance().getEmail() + "/full");

      ContactGroupFeed resultFeed = service.getFeed(feedUrl, ContactGroupFeed.class);

      for (ContactGroupEntry entry : resultFeed.getEntries()) {

        list.add(parseGroup(entry));

      }

    } catch (Exception e) { e.printStackTrace(); }

 

    return list;

  }
Listagem 14. Métodos para gerenciamento de Grupos de Contatos

Com isso concluímos nosso gerenciador de contatos e grupos de contatos usando o serviço ContactsService. Agora temos a infra-estrutura necessária para realizar as operações básicas envolvendo contatos e grupos.

Interface Gráfica

A interface gráfica de nossa aplicação será um conjunto simples de telas feitas em Swing para exibir as listas de contatos e grupos, bem como visualizar e alterar as informações relativas a esses.

Como esse não é o foco principal deste artigo, o código referente à interface gráfica da nossa aplicação não será mostrado aqui; também deixamos de lado vários pontos relevantes numa GUI profissional, como validação e formatação de dados. Mas o código da interface está disponível para download no site da Java Magazine, bem como o código dos módulos que vimos até agora. As Figuras 1 e 2 mostram, respectivamente, a tela principal da nossa aplicação e a tela de visualização / edição dos dados de um contato.

Tela principal da aplicação
Figura 1. Tela principal da aplicação
Tela de visualização / edição dos dados de um contato
Figura 2. Tela de visualização / edição dos dados de um contato

Conclusões

Neste artigo aprendemos, de forma prática, o funcionamento da API de Contatos Google. Vimos como realizar as principais operações que envolvem o gerenciamento dos contatos e grupos de contatos de um usuário e criamos uma aplicação Swing simples, usando a Java Client Library, para testar essas funcionalidades. Nossa aplicação, contudo, possui suas próprias classes de domínio, que é o que acontece na maioria dos casos reais. Usamos também uma camada capaz de abstrair os serviços e tipos específicos da biblioteca, diminuindo assim a dependência com o serviço Google especificamente. Também criamos uma infra-estrutura básica para que possamos acrescentar novos serviços à nossa aplicação.

Com isso concluímos o estudo da primeira das APIs de Dados Google. Nos próximos artigos conheceremos mais alguns dos serviços providos pelas Google Data APIs e os usaremos para incrementar nossa aplicação. Até a próxima!

Saiu na DevMedia!

  • Seja um mestre SQL: Uma verdade sobre o SQL é que ou você já usa, estuda ou um dia, acredite, vai usar. Vem com a gente aprender passo a passo como conversar com o banco de dados nesta série.
  • Entre de cabeça no REST: Você já deve ter notado que o prazo para o lançamento de uma aplicação nem sempre corresponde a complexidade dos seus requisitos. Por esse motivo, é cada vez mais importante que o desenvolvedor saiba como criar e consumir APIs. Veja como nesta série.

Links

Glossário:
  • [1] Aqui iremos mostrar apenas os elementos que são filhos imediatos de Entry. Porém, alguns desses elementos possuem seus próprios sub-elementos. Para uma referência completa consulte a documentação oficial, ver Links.
  • [2] Ao decorrer do artigo utilizaremos os termos Entrada de Contato e Entrada de Grupo para representar os dados correspondentes a um contato ou grupo específicos no servidor.
  • [3] A documentação não cita nenhum caso de multiplicidade “um ou muitos”, ou seja, o elemento deve aparecer pelo menos uma vez, mas pode aparecer várias vezes. Esse tipo de multiplicidade seria representado pelo símbolo “+”.
  • Pode-se usar o valor default no lugar do e-mail para recuperar os dados referentes ao usuário autenticado.
  • Se tentarmos usar um parâmetro de query não suportado pelo serviço, receberemos uma ServiceForbiddenException, com a mensagem no formato:“Fobidden. This service does not support the parameter”.
  • [6] Usando a Java Client Library, para configurar o valor de parâmetros específicos de um serviço, é necessário usar o método Query.addCustomParameter().
  • [7] Atualmente os tipos de autenticação disponíveis são: ClientLogin, AuthSub e OAuth. Para mais detalhes sobre cada um dos tipos de autenticação, veja o artigo anterior.
  • [8] O valor de projeção base é equivalente ao thin. Quando recuperamos os contatos do servidor, os IDs desses contatos sempre usam a projeção base. Atualmente, não existe nenhuma referência a esse valor de projeção na documentação oficial da API.
  • [9] Já vimos anteriormente as opções de parâmetros disponíveis para a API de Contatos.
  • [10] Se não tivéssemos nenhum parâmetro adicional na requisição, poderíamos simplesmente chamar o método getFeed() no objeto service, eliminando a necessidade de um objeto Query.