Skip to content

20. Programação assíncrona com RxJava

Documento a ser lido: [Introduction à RxJava. Application aux environnements Swing et Android.]

Neste capítulo, retomamos o capítulo 17.6, no qual criamos um aplicativo cliente/servidor com a seguinte arquitetura:

Algumas ações do usuário na interface Swing em [1] acionam ações que chegam até o banco de dados em [3] por meio de uma rede HTTP [2]. Devido a isso, a resposta à ação do usuário pode demorar mais ou menos tempo para chegar. Seria bom poder incluir um indicador de espera na interface do usuário com a opção de cancelar a operação iniciada caso ela demore demais. No capítulo 17.6, cada ação do usuário que exija a troca de informações com o servidor é síncrona. O gerenciador de eventos executado pelo código só é concluído quando a resposta é recebida. Durante todo esse tempo, a interface gráfica fica congelada: ela não responde a novas ações do usuário. Essas ações são simplesmente colocadas em fila para serem processadas assim que o gerenciador de eventos que está sendo executado for concluído. Assim, se fosse exibido um botão de cancelamento, o usuário poderia clicar nele, mas nada aconteceria enquanto a operação em andamento não fosse concluída. O botão de cancelamento não teria, então, nenhuma utilidade.

Para que o clique no botão de cancelamento surta efeito, é necessário que a operação em andamento esteja concluída. Para isso, ela deve iniciar a operação potencialmente demorada de forma assíncrona:

  • o gerenciador de eventos inicia a operação demorada, mas não aguarda seu resultado e devolve o controle ao thread do UI, que gerencia os eventos da interface gráfica. A operação demorada é iniciada em um thread diferente do UI, o que não bloqueia este último;
  • se o usuário clicar no botão de cancelamento antes do término da operação demorada, a thread do UI, que está ociosa, pode processar esse evento. É possível, então, abortar a operação demorada ignorando seu resultado;
  • se a operação demorada não tiver sido cancelada, a chegada da resposta provocará um evento na thread do UI. Esta, se estiver ociosa, executará então o código associado a esse evento, que processará a resposta;

A interface do usuário funcionará como antes. Se os tempos de resposta do servidor forem rápidos, o usuário não notará a diferença. Se forem perceptíveis, o usuário verá um botão de cancelamento aparecer e terá a possibilidade de interromper a operação em andamento.

A biblioteca [Rx] permite a programação assíncrona. Seu grande diferencial reside no fato de ter sido portada para diversos ambientes (Java, .NET, JS, ...) e que o domínio dessa biblioteca em um ambiente pode ser facilmente transferido para outro. Vamos nos basear aqui no capítulo 2 do documento [Introduction à RxJava. Application aux environnements Swing et Android]. Recomendamos que o leitor o leia. A seguir, retomaremos o código proveniente dos exemplos desse capítulo.

Vamos desenvolver a arquitetura da aplicação da seguinte maneira:

  • no [1], inserimos uma camada [RxJava] entre a camada [swing] e a camada [métier]. Os métodos dessa camada passarão a ser chamados de forma assíncrona;

Vamos proceder em várias etapas:

  • etapa 1: a camada [metier, DAO] apresenta, por enquanto, uma interface síncrona com a camada [ui]. Vamos transformá-la em uma camada assíncrona [RxJava, metier, DAO];
  • etapa 2: vamos converter o aplicativo de console síncrono em um aplicativo que continue síncrono, mas que utilize a interface assíncrona [RxJava, metier, DAO];
  • etapa 3: converteremos o aplicativo Swing síncrono em um aplicativo Swing assíncrono;

20.1. etapa 1

Transformamos a camada síncrona atual [metier, DAO] em uma camada assíncrona [RxJava, metier, DAO].

20.1.1. Criação

Partimos do projeto Maven do capítulo 17.4, que abrimos com o NetBeans:

Duplicamos esse projeto [1] (copiar/colar) em um novo projeto [elections-rxjava-metier-dao-security-webjson] [2].

20.1.2. Configuração do Maven

Atualizamos o arquivo [pom.xml] do novo projeto para adicionar a dependência da biblioteca [RxJava]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>istia.st.elections</groupId>
  <artifactId>elections-metier-dao-security-rxjava-webjson</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <description>Client jUnit du serveur web / jSON</description>
  <name>elections-metier-dao-security-rxjava-webjson</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.2.7.RELEASE</version>
  </parent>

  <dependencies>
    <!-- Spring -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
    </dependency>
    <!-- biblioteca jSON utilizada pelo Spring -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
    </dependency>
    <!-- componente utilizado pelo Spring RestTemplate -->
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
    </dependency>
    <!-- Google Guava -->
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>16.0.1</version>
      <scope>test</scope>
    </dependency>
    <!-- biblioteca de logs -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <!-- Teste do Spring Boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Spring Boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/io.reactivex/rxjava -->
    <dependency>
      <groupId>io.reactivex</groupId>
      <artifactId>rxjava</artifactId>
      <version>1.2.0</version>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.18.1</version>
      </plugin>
    </plugins>
  </build>
</project>
  • linhas 65-70: adicionamos a dependência da biblioteca RxJava;

20.1.3. Implementação assíncrona da camada [métier]

Para implementar a camada [RxJava, métier], adicionamos uma interface assíncrona [IRxElectionsMetier] [1] e sua implementação [RxElectionsMetier] [2] ao projeto:

  

A interface [IRxElectionsMetier] é a interface assíncrona da camada [RxJava, métier]. Seu código é o seguinte:


package elections.security.client.metier;

import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;

public interface IRxElectionsMetier {

  // autenticação
  Observable<Void> authenticate(User user);

  // obter as listas de candidatos
  Observable<ListeElectorale[]> getListesElectorales(User user);

  // o número de cadeiras a serem preenchidas
  Observable<Integer> getNbSiegesAPourvoir(User user);

  // o limite eleitoral
  Observable<Double> getSeuilElectoral(User user);

  // registro dos resultados
  Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);

  // cálculo dos assentos
  Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);

}

A interface [IRxElectionsMetier] herda os métodos da interface [IElectionsMetier], mas, enquanto um método M da interface [IElectionsMetier] retornava um resultado do tipo T, o método M da interface [IRxElectionsMetier] retorna um resultado do tipo Observable<T>. O tipo [Observable] é fornecido pela biblioteca RxJava. Um tipo Observable<T> fornece o método [subscribe], que obtém o tipo T de forma assíncrona. Estão associados a esse método três eventos:

  • onSuccess(T result), que avisa que um resultado do tipo T está disponível. A operação assíncrona pode fornecer vários resultados;
  • onError(Throwable th), que avisa que a operação assíncrona encontrou um erro;
  • onCompleted(), que avisa que a operação assíncrona foi concluída;

Enquanto o método [Observable.subscribe] não for chamado, a operação assíncrona associada ao observável não será iniciada. O código que chama um método M da interface [IRxElectionsMetier] não obtém o resultado T esperado, mas um tipo Observable<T> que lhe permitirá, posteriormente, obter o resultado T ao chamar o método [Observable.subscribe].

A implementação [RxElectionsMetier] da interface [IRxElectionsMetier] é a seguinte:


package elections.security.client.metier;

import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;

@Component
public class RxElectionsMetier implements IRxElectionsMetier {

  @Autowired
  private IElectionsMetier metier;

  @Override
  public Observable<Void> authenticate(User user) {
    ...
  }

  @Override
  public Observable<ListeElectorale[]> getListesElectorales(User user) {
    return Observable.create(subscriber -> {
      try {
        // chamada do método síncrono seguida de resposta ao assinante
        subscriber.onNext(metier.getListesElectorales(user));
        // é sinalizado o fim do observável
        subscriber.onCompleted();
      } catch (Exception e) {
        // a exceção é encaminhada
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Integer> getNbSiegesAPourvoir(User user) {
    ...
  }

  @Override
  public Observable<Double> getSeuilElectoral(User user) {
    ...
  }

  @Override
  public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
    ...
  }

  @Override
  public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
    ...
  }
}
  • linhas 12-13: injeção Spring da camada de negócios síncrona;
  • linhas 20-34: vamos comentar o método [getListesElectorales], que, em vez de retornar um tipo [ListeElectorale[]], retorna um tipo [Observable<ListeElectorale[]>];
  • linhas 22-32: o método estático [Observable.create] permite criar um Observable a partir de um tipo [Subscriber]. O tipo [Subscriber] representa um assinante dos fluxos de resultados produzidos pelo processo observado (o Observable). Ele fornece três métodos:
    • [Subscriber.onNext] (linha 25) para receber um resultado do processo observado;
    • [Subscriber.onError] (linha 30) para receber uma exceção do processo observado. Após uma exceção, o tipo [Observable] não emite mais resultados;
    • [Subscriber.onCompleted] (linha 27) para receber o sinal de fim de emissão do processo observado. Aqui, o processo observado emite apenas um elemento. Observe-se que esse sinal não é emitido caso ocorra uma exceção. Esse é o comportamento padrão dos Observáveis: a emissão de uma exceção também sinaliza o fim das emissões. Os assinantes sabem disso;
  • linhas 22-34: o método [Observable.create] aceita como parâmetro um tipo [Observable.OnSubscribe]. Esse tipo é uma interface funcional. Esse conceito foi introduzido com o Java 8 e designa uma interface com um único método. Aqui, o único método da interface [Observable.OnSubscribe] é o seguinte:
T call(Subscriber<T> subscriber)

Para implementar uma interface funcional com um único método m(param1, param2, ..., paramn), pode-se utilizar a seguinte sintaxe simplificada:

(param1, param2, ..., paramn) -> { code de la méthode m}

É isso que é feito nas linhas 22 a 34:

  • [subscriber] é o parâmetro do método [Observable.OnSubscribe.call];
  • linhas 23 a 32: o código que se deseja atribuir ao método [call];
  • linha 25: solicita-se as listas eleitorais de forma síncrona à camada [métier] injetada na linha 13. Portanto, haverá uma espera pelo resultado. Quando este for recebido, ele é passado para o método [onNext] do assinante;
  • linha 28: em caso de erro, a exceção é passada para o método [onError] do assinante;
  • linha 31: aguarda-se apenas um resultado. Quando este for obtido (as listas eleitorais ou uma exceção), indica-se ao assinante que o processo observado concluiu a emissão de resultados;

É importante lembrar que o método [RxElectionsMetier] retorna um tipo Observable<ListeElectorale[]> e não o próprio tipo ListeElectorale[]. Será necessário que o código chamador chame o método Observable<ListeElectorale[]>.subscribe para que o código das linhas 23 a 33 seja executado e retorne as listas eleitorais por meio da linha 25.

O código dos demais métodos é semelhante:


package elections.security.client.metier;

import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;

@Component
public class RxElectionsMetier implements IRxElectionsMetier {

  @Autowired
  private IElectionsMetier metier;

  @Override
  public Observable<Void> authenticate(User user) {
    return Observable.create(subscriber -> {
      try {
        // chamada de método síncrono
        metier.authenticate(user);
        // é sinalizado o fim do observável
        subscriber.onCompleted();
      } catch (Exception e) {
        // a exceção é encaminhada
        subscriber.onError(e);
      }
    });

  }

  @Override
  public Observable<ListeElectorale[]> getListesElectorales(User user) {
    return Observable.create(subscriber -> {
      try {
        // chamada de método síncrono seguida de resposta ao assinante
        subscriber.onNext(metier.getListesElectorales(user));
        // é sinalizado o fim do observável
        subscriber.onCompleted();
      } catch (Exception e) {
        // a exceção é encaminhada
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Integer> getNbSiegesAPourvoir(User user) {
    return Observable.create(subscriber -> {
      try {
        // chamada de método síncrono e, em seguida, resposta ao assinante
        subscriber.onNext(metier.getNbSiegesAPourvoir(user));
        // é sinalizado o fim do observável
        subscriber.onCompleted();
      } catch (Exception e) {
        // a exceção é encaminhada
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Double> getSeuilElectoral(User user) {
    return Observable.create(subscriber -> {
      try {
        // chamada de método síncrono e, em seguida, resposta ao assinante
        subscriber.onNext(metier.getSeuilElectoral(user));
        // é sinalizado o fim do observável
        subscriber.onCompleted();
      } catch (Exception e) {
        // a exceção é encaminhada
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // chamada de método síncrono
        metier.recordResultats(user, listesElectorales);
        // é sinalizado o fim do observável
        subscriber.onCompleted();
      } catch (Exception e) {
        // a exceção é encaminhada
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // chamada de método síncrono seguida de resposta ao assinante
        subscriber.onNext(metier.calculerSieges(user, listesElectorales));
        // é sinalizado o fim do observável
        subscriber.onCompleted();
      } catch (Exception e) {
        // a exceção é encaminhada
        subscriber.onError(e);
      }
    });
  }
}
  • linhas 20 e 81: o método [onNext] do assinante não é chamado porque este não espera resultados;

20.1.4. Os testes JUnit da camada [métier]

  

20.1.4.1. Test01

Retomamos o teste unitário [Test01] analisado no parágrafo 17.4.4. Ele foi projetado para realizar chamadas síncronas à interface [IElectionsMetier]. Nós o modificamos para que ele faça chamadas síncronas à nova interface [IRxElectionsMetier]. De fato, é possível fazer chamadas síncronas a uma interface assíncrona RxJava. O código fica da seguinte forma:


package elections.security.client.metier.junit;

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.security.client.config.MetierConfig;
import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import rx.observables.BlockingObservable;

@SpringApplicationConfiguration(classes = MetierConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

  // camada [electionsMetier]
  @Autowired
  private IRxElectionsMetier electionsMetier;

  // mapeador jSON
  private final ObjectMapper mapper = new ObjectMapper();

  // usuários
  static private User admin;
  static private User user;
  static private User unknown;

  @BeforeClass
  public static void initTest() {
    admin = new User("admin", "admin");
    user = new User("user", "user");
    unknown = new User("x", "y");
  }

  @Test()
  public void checkUserUser() {
    ElectionsException se = null;
    try {
      BlockingObservable.from(electionsMetier.authenticate(user)).firstOrDefault(null);
    } catch (ElectionsException e) {
      se = e;
    }
    Assert.assertNotNull(se);
    Assert.assertEquals("403 Forbidden", se.getErreurs().get(0));
  }

  @Test()
  public void checkUserUnknown() {
    ElectionsException se = null;
    try {
      BlockingObservable.from(electionsMetier.authenticate(unknown)).firstOrDefault(null);
    } catch (ElectionsException e) {
      se = e;
    }
    Assert.assertNotNull(se);
    Assert.assertEquals("401 Unauthorized", se.getErreurs().get(0));
  }

  @Test()
  public void checkUserAdmin() {
    ElectionsException se = null;
    try {
      BlockingObservable.from(electionsMetier.authenticate(admin)).firstOrDefault(null);
    } catch (ElectionsException e) {
      se = e;
    }
    Assert.assertNull(se);
  }

  /**
   * vérification 1 : méthode de calcul des sièges on fixe en dur les listes
   */
  @Test
  public void calculSieges1() {
    // cria-se a tabela das 7 listas de candidatos
    ListeElectorale[] listes = new ListeElectorale[7];
    listes[0] = new ListeElectorale("A", 32000, 0, false);
    listes[1] = new ListeElectorale("B", 25000, 0, false);
    listes[2] = new ListeElectorale("C", 16000, 0, false);
    listes[3] = new ListeElectorale("D", 12000, 0, false);
    listes[4] = new ListeElectorale("E", 8000, 0, false);
    listes[5] = new ListeElectorale("F", 4500, 0, false);
    listes[6] = new ListeElectorale("G", 2500, 0, false);
    // calculam-se os assentos de cada uma das listas
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // verifica-se os resultados
    Assert.assertEquals(2, listes[0].getSieges());
    Assert.assertFalse(listes[0].isElimine());
    Assert.assertEquals(2, listes[1].getSieges());
    Assert.assertFalse(listes[1].isElimine());
    Assert.assertEquals(1, listes[2].getSieges());
    Assert.assertFalse(listes[2].isElimine());
    Assert.assertEquals(1, listes[3].getSieges());
    Assert.assertFalse(listes[3].isElimine());
    Assert.assertEquals(0, listes[4].getSieges());
    Assert.assertFalse(listes[4].isElimine());
    Assert.assertEquals(0, listes[5].getSieges());
    Assert.assertTrue(listes[5].isElimine());
    Assert.assertEquals(0, listes[6].getSieges());
    Assert.assertTrue(listes[6].isElimine());
  }

  /**
   * vérification 2 : méthode de calcul des sièges on demande les listes à la couche [metier] puis on fixe en dur les
   * voix
   */
  @Test
  public void calculSieges2() {
    // cria-se a tabela das 7 listas de candidatos
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // fixa-se o número de votos
    listes[0].setVoix(32000);
    listes[1].setVoix(25000);
    listes[2].setVoix(16000);
    listes[3].setVoix(12000);
    listes[4].setVoix(8000);
    listes[5].setVoix(4500);
    listes[6].setVoix(2500);
    // calcula-se o número de cadeiras obtidas por cada uma das listas
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // verifica-se os resultados
    Assert.assertEquals(2, listes[0].getSieges());
    Assert.assertFalse(listes[0].isElimine());
    Assert.assertEquals(2, listes[1].getSieges());
    Assert.assertFalse(listes[1].isElimine());
    Assert.assertEquals(1, listes[2].getSieges());
    Assert.assertFalse(listes[2].isElimine());
    Assert.assertEquals(1, listes[3].getSieges());
    Assert.assertFalse(listes[3].isElimine());
    Assert.assertEquals(0, listes[4].getSieges());
    Assert.assertFalse(listes[4].isElimine());
    Assert.assertEquals(0, listes[5].getSieges());
    Assert.assertTrue(listes[5].isElimine());
    Assert.assertEquals(0, listes[6].getSieges());
    Assert.assertTrue(listes[6].isElimine());
  }

  /**
   * vérification 3 méthode de calcul des sièges on provoque une exception
   */
  @Test(expected = ElectionsException.class)
  public void calculSieges3() {
    // cria-se uma tabela com 24 listas candidatas, cada uma com 1 voto
    ListeElectorale[] listes = new ListeElectorale[25];
    // as 25 listas terão o mesmo número de votos (4%)
    for (int i = 0; i < listes.length; i++) {
      listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
    }
    // cálculo dos assentos — normalmente, deve-se obter um ElectionsException
    // com um limite eleitoral de 5%
    BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
  }

  /**
   * enregistrement des résultats de l'élection
   *
   * @throws JsonProcessingException
   */
  @Test
  public void ecritureResultatsElections() throws JsonProcessingException {
    // cria-se a tabela das 7 listas candidatas
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // definimos os votos de forma fixa
    listes[0].setVoix(32000);
    listes[1].setVoix(25000);
    listes[2].setVoix(16000);
    listes[3].setVoix(12000);
    listes[4].setVoix(8000);
    listes[5].setVoix(4500);
    listes[6].setVoix(2500);
    // calcula-se o número de cadeiras obtidas por cada uma das listas
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // exibe-se os resultados
    for (int i = 0; i < listes.length; i++) {
      System.out.println(mapper.writeValueAsString(listes[i]));
    }
    // os resultados são gravados no banco de dados
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
    // verifica-se os resultados
    listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // exibe-se os resultados
    for (int i = 0; i < listes.length; i++) {
      System.out.println(mapper.writeValueAsString(listes[i]));
    }
    Assert.assertEquals(2, listes[0].getSieges());
    Assert.assertFalse(listes[0].isElimine());
    Assert.assertEquals(2, listes[1].getSieges());
    Assert.assertFalse(listes[1].isElimine());
    Assert.assertEquals(1, listes[2].getSieges());
    Assert.assertFalse(listes[2].isElimine());
    Assert.assertEquals(1, listes[3].getSieges());
    Assert.assertFalse(listes[3].isElimine());
    Assert.assertEquals(0, listes[4].getSieges());
    Assert.assertFalse(listes[4].isElimine());
    Assert.assertEquals(0, listes[5].getSieges());
    Assert.assertTrue(listes[5].isElimine());
    Assert.assertEquals(0, listes[6].getSieges());
    Assert.assertTrue(listes[6].isElimine());
  }
}

Vamos analisar as alterações:

  • linha 48: o método estático [BlockingObservable.from(Observable).first]:
    • assina o parâmetro observável de [from];
    • inicia a execução do código associado ao observável;
    • aguarda o recebimento do primeiro resultado. Trata-se, portanto, de uma operação síncrona;

Usamos aqui o método [firstOrDefault(null)] porque o observável [metier.authenticate] não retorna nenhum resultado quando executado. O resultado do método [firstOrDefault(null)] será, portanto, null, valor não utilizado aqui;

Repetimos esse esquema no restante do código sempre que quisermos chamar a camada [métier].

O teste unitário [Test01] deve ser aprovado:

 

Tarefa a ser realizada: verificar se o teste [Test01] é aprovado.


20.1.4.2. Test02

Estamos adaptando o teste [Test01] para, a partir de agora, testar a interface assíncrona [IRxElectionsMetier] por meio de chamadas assíncronas aos seus métodos.

Vamos analisar um primeiro teste:


  // semáforo de sincronização de threads
  private CountDownLatch latch;

  // -----------------------------------
  private ElectionsException checkUserUserException;

  @Test()
  public void checkUserUser() throws InterruptedException {
    // semáforo em 1
    latch = new CountDownLatch((1));
    // operação assíncrona
    electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // espera pelo semáforo
    latch.await();
    // verificação dos resultados
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • linha 2: um semáforo é uma ferramenta usada para sincronizar threads entre si. Threads são fluxos de execução que ocorrem paralelamente. Para executar uma tarefa T1, o thread [Thread1] pode precisar que uma tarefa T2, executada por um thread [Thread2], seja concluída. Ele então aguarda que a thread [Thread2] lhe envie um sinal indicando que a tarefa T2 foi concluída. Existem diversas maneiras de gerenciar essa sincronização entre duas threads. O método utilizado aqui é o seguinte:
    • linha 10: a thread [Thread1] cria um semáforo com o valor 1;
    • linha 12: o thread [Thread1] cria e inicia um thread [Thread2]. Isso é feito por meio da sintaxe:

electionsMetier.authenticate(user).subscribeOn(Schedulers.io())

O método [Observable.subscribeOn] define a thread na qual o processo observado será executado. O parâmetro de [subscribeOn] é um pool de threads. A biblioteca RxJava fornece vários deles, adequados a diferentes situações. O pool [Schedulers.io()] é o recomendado para operações de rede;

  • (continuação)
    • linhas 12-13: a operação

electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)

executa a operação síncrona encapsulada no observável [authenticate(user)]. Mas, como essa operação síncrona é iniciada em um thread diferente do thread [Thread1], este último não aguarda a resposta do método [subscribe] e passa para a instrução seguinte;

  • (continuação)
    • linha 23: a thread [Thread1] é interrompida e aguarda que o semáforo passe para 0 (ele está em 1 no momento);
  • linhas 13-21: o método [subscribe] recebe como parâmetros três funções lambda:
    • a primeira, [(result)->{...}], é chamada sempre que o observável [authenticate(user)] emite um resultado [result]. Aqui temos um observável [authenticate(user)] que realiza alguma ação, mas não emite nenhum resultado. A função lambda [(result)->{}], portanto, nunca será chamada. É por isso que seu código está vazio aqui: [{}];
    • o segundo, [(th)->{...}], recebe como parâmetro um tipo [Throwable]. Ele é chamado quando a execução do observável encontra uma exceção. Aqui, tratamos o parâmetro [Throwable th] da seguinte maneira:
      • linha 16: armazenamos esse valor em um campo da classe de teste do tipo [ElectionsException], pois o observável executado emite apenas esse tipo de exceção;
      • linha 17: definimos o semáforo como 0 para indicar que a thread [Thread2] concluiu seu trabalho;
    • o terceiro, [()->{...}], é chamado quando o observável não tem mais elementos para emitir. Tratamos esse evento da seguinte maneira:
      • linha 20: colocamos o semáforo em 0 para indicar que a thread [Thread2] concluiu seu trabalho;

É importante observar que a terceira função lambda não é chamada se ocorrer uma exceção. Por isso, fomos obrigados a definir o semáforo como 0 também na linha 17;

  • linha 25: quando chegamos a esta linha, o observável concluiu seu trabalho. Podemos então realizar as mesmas verificações que no teste [Test01];

Vamos examinar outro teste:


// -----------------------------------
  private ElectionsException calculSieges1Exception;
  private ListeElectorale[] listesCalculSieges1;

  @Test
  public void calculSieges1() throws InterruptedException {
    // cria-se a matriz com as 7 listas candidatas
    ListeElectorale[] listes = new ListeElectorale[7];
    listes[0] = new ListeElectorale("A", 32000, 0, false);
    listes[1] = new ListeElectorale("B", 25000, 0, false);
    listes[2] = new ListeElectorale("C", 16000, 0, false);
    listes[3] = new ListeElectorale("D", 12000, 0, false);
    listes[4] = new ListeElectorale("E", 8000, 0, false);
    listes[5] = new ListeElectorale("F", 4500, 0, false);
    listes[6] = new ListeElectorale("G", 2500, 0, false);
    // semáforo em 1
    latch = new CountDownLatch((1));
    // operação assíncrona    
    // cálculo dos assentos de cada uma das listas
    electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // espera pelo semáforo
    latch.await();
    // verifica-se os resultados
    Assert.assertNull(calculSieges1Exception);
    Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
    Assert.assertFalse(listesCalculSieges1[0].isElimine());
    Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
    Assert.assertFalse(listesCalculSieges1[1].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
    Assert.assertFalse(listesCalculSieges1[2].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
    Assert.assertFalse(listesCalculSieges1[3].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
    Assert.assertFalse(listesCalculSieges1[4].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
    Assert.assertTrue(listesCalculSieges1[5].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
    Assert.assertTrue(listesCalculSieges1[6].isElimine());
  }
  • linhas 20-30: execução assíncrona do observável [electionsMetier.calculerSieges(admin, listes)];
  • linhas 21-23: a execução do observável retorna um tipo [ListeElectorale[]] que é armazenado em um campo da classe de teste, linha 3;
  • linhas 34-48: essas verificações são as do teste [Test01], às quais foi adicionada a verificação da linha 34, que garante que não tenha ocorrido nenhuma exceção;

O teste [Test02] completo está disponível no material didático.


Tarefa a ser realizada: executar o teste [Test02] e verificar se ele é aprovado.


20.1.4.3. Test03

O teste [Test03] faz o mesmo que o teste [Test01]: ele testa a interface [IRxElectionsMetier] por meio de chamadas síncronas a essa interface. Trata-se de uma cópia do teste [Test02], com duas diferenças:

  • os observáveis não são mais executados em uma thread diferente daquela que executa os testes. Quando a thread [Thread1] executa o método [subscribe] de um observável, este inicia uma operação HTTP para o servidor, também na thread [Thread1]. Todo o método [subscribe] torna-se, então, síncrono;
  • uma vez que há apenas uma thread, a sincronização de threads torna-se desnecessária e o semáforo desaparece;

Aqui estão dois exemplos de testes:


  // -----------------------------------
  private ElectionsException checkUserUserException;

  @Test()
  public void checkUserUser() throws InterruptedException {
    // operação síncrona
    electionsMetier.authenticate(user)
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                    },
                    () -> {
                    });
    // verificação dos resultados
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • linha 7: por padrão, o método [electionsMetier.authenticate(user).subscribe] é executado no thread do código chamador. Portanto, temos uma operação síncrona;

  // -----------------------------------
  private ElectionsException calculSieges1Exception;
  private ListeElectorale[] listesCalculSieges1;

  @Test
  public void calculSieges1() throws InterruptedException {
    // cria-se a tabela das 7 listas de candidatos
    ListeElectorale[] listes = new ListeElectorale[7];
    listes[0] = new ListeElectorale("A", 32000, 0, false);
    listes[1] = new ListeElectorale("B", 25000, 0, false);
    listes[2] = new ListeElectorale("C", 16000, 0, false);
    listes[3] = new ListeElectorale("D", 12000, 0, false);
    listes[4] = new ListeElectorale("E", 8000, 0, false);
    listes[5] = new ListeElectorale("F", 4500, 0, false);
    listes[6] = new ListeElectorale("G", 2500, 0, false);
    // operação síncrona    
    // calcula-se o número de cadeiras de cada uma das listas
    electionsMetier.calculerSieges(admin, listes)
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                    },
                    () -> {
                    });
    // verificação dos resultados
    Assert.assertNull(calculSieges1Exception);
    Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
    Assert.assertFalse(listesCalculSieges1[0].isElimine());
    Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
    Assert.assertFalse(listesCalculSieges1[1].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
    Assert.assertFalse(listesCalculSieges1[2].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
    Assert.assertFalse(listesCalculSieges1[3].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
    Assert.assertFalse(listesCalculSieges1[4].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
    Assert.assertTrue(listesCalculSieges1[5].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
    Assert.assertTrue(listesCalculSieges1[6].isElimine());
  }

Tarefa a ser realizada: passar o teste [Test03] e verificar se ele é aprovado.


20.2. Etapa 2

Agora, vamos portar o aplicativo de console síncrono do capítulo 17.5 para um aplicativo que continue síncrono, mas que utilize a interface assíncrona [RxJava, metier, DAO];

Partimos do projeto [elections-console-metier-dao-security-webjson] [1] do capítulo 17.5, que duplicamos em um novo projeto [elections-console-rxjava- metier-dao-security-webjson] [2]:

  • em [3-4]; no novo projeto, removemos a dependência da camada antiga [métier] síncrona;
  • em [5-9], adiciona-se uma dependência da nova camada assíncrona [métier];
  • em [10-14], renomeia-se a classe [ElectionsConsole] para [ElectionsConsole01];

Da mesma forma, renomeia-se a classe [BootElectionsConsole] para [BootElectionsConsole01]:

 

O código atual da classe [BootElectionsConsole01] é o seguinte:


package elections.security.client.boot;

import elections.security.client.console.IElectionsUI;


public class BootElectionsConsole01 extends AbstractBootElections{
    public static void main(String[] arguments) {
        new BootElectionsConsole01().run();
    }

    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsConsole",IElectionsUI.class);
    }
}
  • linha 13: como o nome da classe [ElectionsConsole] foi alterado para [ElectionsConsole01], agora é necessário escrever:

        return ctx.getBean("electionsConsole01",IElectionsUI.class);

Voltemos ao código da classe [ElectionsConsole01]:


@Component
public class ElectionsConsole01 implements IElectionsUI {

    @Autowired
    private IElectionsMetier electionsMetier;

  @Autowired
  private User admin;
  
    @Override
    public void run() {
        // as listas em disputa
        ListeElectorale[] listes;
        // inserção de dados
        try (Scanner clavier = new Scanner(System.in)) {
         // solicita-se as listas em disputa à camada [metier]
         listes = electionsMetier.getListesElectorales(admin);
            ...
        // realiza-se o cálculo dos assentos
        listes=electionsMetier.calculerSieges(admin,listes);
        // os resultados são registrados
        electionsMetier.recordResultats(admin,listes);
        ...
}

Se seguirmos o exemplo do teste [Test01] do parágrafo 20.1.4.1, as linhas 5, 17, 20 e 22 sofrerão as seguintes alterações:


@Component
public class ElectionsConsole01 implements IElectionsUI {

  @Autowired
  private IRxElectionsMetier electionsMetier;

  @Autowired
  private User admin;

  @Override
  public void run() {
    // as listas em disputa
    ListeElectorale[] listes;
    // entrada de dados
    try (Scanner clavier = new Scanner(System.in)) {
      // solicita-se as listas em disputa à camada [metier]
      listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
      ...
    // realiza-se o cálculo dos assentos
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // os resultados são registrados
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
    ...
  }

Tarefa: configure o projeto para executar a classe [BootElectionsConsole01] com os três parâmetros [SS, Heures travaillées, Jours travaillés] e verifique se a execução do projeto assim configurado produz os resultados esperados.



Tarefa: configure o projeto para executar o par [BootElectionsConsole02, ElectionsConsole02], em que a classe [ElectionsConsole02] terá sido escrita seguindo o modelo do teste [Test02] do parágrafo 20.1.4.2.



Tarefa: configure o projeto para executar o par [BootElectionsConsole03, ElectionsConsole03], em que a classe [ElectionsConsole03] terá sido escrita seguindo o modelo do teste [Test03] do parágrafo 20.1.4.3.


20.3. Etapa 3

Passamos agora à adaptação da aplicação Swing para um ambiente assíncrono.

Começamos duplicando o projeto [elections-swing-metier-dao-security-webjson] [1] do capítulo 17.6 em um novo projeto [elections-swing-rxjava-metier-dao-security-webjson] [2]:

  • no [3, 4], removemos a dependência da camada síncrona [console];
  • em [5-9], adicionamos uma dependência da camada de console assíncrona;

A camada [swing] fará chamadas assíncronas reais para a camada [métier]. Ao chamar um método desta última, haverá duas threads:

  • a thread da UI, que gerencia os eventos;
  • um thread de E/S que executará a chamada HTTP ao servidor;

Durante toda a duração da chamada assíncrona, deveríamos exibir uma imagem de espera, bem como um botão de cancelamento. Não faremos isso aqui, e isso será proposto a você como uma melhoria do aplicativo. As modificações ocorrem nas duas classes que fazem chamadas para a camada [métier]:

 

20.3.1. Configuração do Maven

Usaremos aqui a biblioteca [RxSwing], que adiciona à biblioteca [RxJava] funcionalidades disponíveis apenas em um ambiente Swing. Para isso, modificamos o arquivo [pom.xml] da seguinte maneira:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>istia.st.elections</groupId>
  <artifactId>elections-swing-rxjava-metier-dao-security-webjson</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>elections-swing-rxjava-metier-dao-security-webjson</name>
  <description>couche swing asynchrone du client web / jSON</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- RxSwing -->
    <!-- https://mvnrepository.com/artifact/io.reactivex/rxswing -->
    <dependency>
      <groupId>io.reactivex</groupId>
      <artifactId>rxswing</artifactId>
      <version>0.27.0</version>
    </dependency>
    <!-- camadas inferiores -->
    <dependency>
      <groupId>istia.st.elections</groupId>
      <artifactId>elections-console-rxjava-metier-dao-security-webjson</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>

</project>

20.3.2. A classe [ElectionsConnectForm]

Em um funcionamento assíncrono, a classe [ElectionsConnectForm] passa a ter a seguinte forma:


package elections.security.client.swing;

import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.User;

import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.SwingUtilities;

import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;

@Component
public class ElectionsConnectForm extends AbstractElectionsConnectForm implements IElectionsUI {

  private static final long serialVersionUID = 1L;

  // referência na camada [métier] assíncrona
  @Autowired
  private IRxElectionsMetier metier;

  // usuário conectado
  private User user;

  // formulário principal
  @Autowired
  private ElectionsMainForm electionsMainForm;

  // sessão UI
  @Autowired
  private UiSession uiSession;

  @Override
  protected void doConnect() {
    if (isPageValid()) {
      // autenticação do usuário
      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
        // não há resposta
        (result) -> {
        },
        // gestão de exceção
        (th) -> {
          // registro do erro
          String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
          // exibindo a informação
          jTextPaneErreurs.setText(info);
          jTextPaneErreurs.setCaretPosition(0);

        },
        // a autenticação foi concluída
        () -> {
          // o usuário é armazenado na sessão
          uiSession.setUser(user);
          // a tela de login é ocultada
          setVisible(false);
          // a tela principal é exibida
          electionsMainForm.run();
        });
    }
  }

  // inicializações
  @Override
  protected void init() {
    ...
  }

  @Override
  public void run() {
    // a interface gráfica é exibida
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
  }

  private boolean isPageValid() {
    ...
  }

  private String getInfoForException(String message, Throwable ex) {
    ...
  }

}
  • linhas 36-63: o método [doConnect] é executado quando o usuário seleciona a opção de menu [Connexion]:
 

Tudo está na linha 40:


      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
  • o processo observado é [metier.authenticate(user)];
  • ele será executado em um thread de E/S retirado do pool [Schedulers.io()];
  • ele será observado na thread UI, que gerencia os eventos da interface Swing [observeOn(SwingScheduler.getInstance())]. Essa thread é obtida pelo método [SwingScheduler.getInstance()], em que [SwingScheduler] é uma classe fornecida pela biblioteca [RxSwing]. Isso é obrigatório. Ao obter o resultado da operação assíncrona, este é frequentemente usado para modificar elementos da interface Swing. No entanto, a interface só pode ser modificada na thread do UI; caso contrário, ocorre uma exceção. Portanto, as linhas 41 a 61 devem ser executadas na thread do UI. Isso é garantido aqui pelo método [observeOn(SwingScheduler.getInstance())];

Vamos comentar o restante do código:

  • linhas 42-43: essas linhas estão lá para respeitar a sintaxe do método [subscribe]. Elas nunca serão executadas, pois o processo [metier.authenticate(user)] não retorna nenhum resultado;
  • linhas 35-52: ao receber uma exceção, ela é exibida;
  • linhas 54-61: executadas quando o processo [metier.authenticate(user)] sinaliza o fim de suas emissões;

20.3.3. A turma [ElectionsMainForm]

 

20.3.3.1. Inicialização da interface gráfica


package elections.security.client.swing;

import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

@Component
public class ElectionsMainForm extends AbstractElectionsMainForm implements IElectionsUI {

  private static final long serialVersionUID = 1L;

  // referência à camada assíncrona [métier]
  @Autowired
  private IRxElectionsMetier metier;

  // sessão UI
  @Autowired
  private UiSession uiSession;

  // usuário conectado
  private User user;

  // modelos das listas JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // listas em competição
  private ListeElectorale[] listes;

  // listas inseridas pelo usuário
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

  // inicializações
  @Override
  protected void init() {
    // geração de componentes pela classe pai
    super.init();
    // estado do formulário
    Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
    Utilitaires.setEnabled(
            new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
    // centralizar a janela
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension frameSize = getSize();
    if (frameSize.height > screenSize.height) {
      frameSize.height = screenSize.height;
    }
    if (frameSize.width > screenSize.width) {
      frameSize.width = screenSize.width;
    }
    setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
    // usuário conectado
    user = uiSession.getUser();
    // inicializações locais
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    // solicitação de listas à camada [métier]
    metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // resposta
                    listesElectorales -> {
                      // as listas são armazenadas
                      listes = listesElectorales;
                    },
                    // exceção
                    (th) -> showException(th),
                    // fim observável
                    () -> {
                      // próxima etapa
                      doInitStep2();
                    });
  }
...
  • linha 46: o método [init] é executado quando a janela associada for exibida. Seu objetivo é inicializar os componentes [1-3] abaixo:
 
  • linhas 71-85: as listas de candidatos são solicitadas de forma assíncrona (componente [1]);
  • linha 71: o processo observado é o [metier.getListesElectorales(user)]. Ele é executado em um thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
  • linhas 74-77: o resultado retornado pelo processo observado é armazenado no campo [listes] da linha 38;
  • linha 79: qualquer exceção é tratada pelo método a seguir:

  private void showException(Throwable th) {
    // exibe-se a exceção
    jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
    jTextPaneMessages.setCaretPosition(0);
}
  • linhas 81-84: ao final do processo observado, executam-se as linhas 81-84. Essas linhas não são executadas caso tenha ocorrido uma exceção. O método [doInitStep2] realiza a etapa 2 da inicialização da seguinte forma:

  private void doInitStep2() {
    // associa-se os nomes das listas ao menu suspenso jComboBoxNomsListes
    for (int i = 0; i < listes.length; i++) {
      jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
    }
    // número de vagas a serem preenchidas
    metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // resposta
                    nbSiegesAPourvoir -> {
                      // inicializa-se o rótulo associado a essa informação
                      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
                    },
                    // exceção
                    (th) -> showException(th),
                    // fim observável
                    () -> {
                      // próxima etapa
                      doInitStep3();
                    });
}
  • linhas 3-5: utiliza-se o resultado da etapa anterior para preencher a lista suspensa com os nomes das listas de candidatos;
  • linhas 7-20: solicita-se o número de vagas a serem preenchidas de forma assíncrona;
  • linha 7: o processo observado é o [metier.getNbSiegesAPourvoir(user)]. Ele é executado em um thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
  • linhas 10-13: o resultado retornado pelo processo é usado para atualizar a interface gráfica;
  • linha 15: exibe-se a eventual exceção;
  • linhas 17-20: ao receber o sinal de fim do observável, passa-se para a etapa 3 do processo de inicialização;

A etapa 3 da inicialização é executada pelo código a seguir:


  private void doInitStep3() {
    // limiar eleitoral
    metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // resposta
                    seuilElectoral -> {
                      // inicializa-se o rótulo associado a essa informação
                      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
                    },
                    // exceção
                    (th) -> showException(th),
                    // fim da observação
                    () -> {
                    });
}
  • linhas 3-4: solicita-se o limite eleitoral de forma assíncrona;
  • linha 3: o processo observado é [metier.getSeuilElectoral(user)]. Ele é executado em um thread de E/S [subscribeOn(Schedulers.io())] e observado no thread UI [observeOn(SwingScheduler.getInstance()];
  • linhas 6-9: o resultado retornado pelo processo é usado para atualizar a interface gráfica;
  • linha 11: exibe-se a eventual exceção;
  • linhas 13-14: ao receber o sinal de fim do observável, nada é feito: o processo de inicialização da interface gráfica está concluído;

20.3.3.2. Cálculo dos assentos obtidos pelas diferentes listas

O método [doCalculer] tem como função calcular o número de cadeiras obtidas pelas diferentes listas:


  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // cálculo de assentos
    String info = null;
    metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // processamento do resultado
                    result -> consumeResultSieges(result),
                    // processamento de exceção
                    th -> showException(th),
                    // fim observável
                    () -> {
                    }
            );
}
  • linhas 6-15: calcula-se de forma assíncrona os assentos obtidos pelas diferentes listas;
  • linha 6: o processo observado é o [metier.calculerSieges(user, tListesSaisies)]. Ele é executado em um thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
  • linha 9: o resultado retornado pelo processo é utilizado pelo método [consumeResultSieges];
  • linha 11: exibe-se a eventual exceção;
  • linhas 13-14: ao receber o sinal de fim do observável, nada é feito;

Linha 9: o método [consumeResultSieges] utiliza o resultado retornado pelo processo observado, as listas de candidatos com seus campos [sieges, elimine] atualizados:


  private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
    // o resultado é armazenado
    this.tListesSaisies = tListesSaisies;
    // exibição dos resultados
    modèleRésultats.clear();
    for (int i = 0; i < tListesSaisies.length; i++) {
      modèleRésultats.addElement(tListesSaisies[i].toString());
    }
    // atualização do status do formulário
    Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, true);
    Utilitaires.setEnabled(new JLabel[]{jLabelCalculer}, false);
    Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, true);
    Utilitaires.setEnabled(new JMenuItem[]{jMenuItemCalculer}, false);
    jTextPaneMessages.setText("Calcul terminé");
}
  • linhas 4-14: o resultado obtido é usado para atualizar a interface gráfica;

20.3.3.3. Registro dos resultados da eleição

O registro dos resultados da eleição é feito pelo método [doEnregistrer] a seguir:


  @Override
  protected void doEnregistrer() {
    // solicita-se o registro à camada [métier]
    metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // processamento do resultado — não há nenhum aqui
                    (param) -> {
                    },
                    // processamento da exceção
                    (th) -> showException(th),
                    // fim observável
                    () -> {
                      // atualização do formulário
                      Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
                      Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
                      jTextPaneMessages.setText("Enregistrement des résultats réalisé");
                    }
            );
}
  • linhas 4-17: os resultados da eleição são registrados de forma assíncrona;
  • linha 4: o processo observado é [metier.recordResultats(user, tListesSaisies)]. Ele é executado em um thread de E/S [subscribeOn(Schedulers.io())] e observado no thread do UI [observeOn(SwingScheduler.getInstance()];
  • linhas 7-8: essas linhas nunca serão executadas, pois o processo observado não retorna nenhum resultado;
  • linha 10: exibe-se a eventual exceção;
  • linhas 14-16: ao receber o sinal de fim do observável, atualiza-se a interface gráfica;

Tarefa a ser realizada: verifique se o aplicativo Swing está funcionando. Em seguida, adapte a interface gráfica e o código para que, durante uma operação assíncrona com o servidor web / jSON, apareça uma imagem de espera, bem como uma opção para cancelar a operação em andamento.