Skip to content

20. Programación asíncrona con RxJava

Documento para leer: [Introduction à RxJava. Application aux environnements Swing et Android.]

En este capítulo, retomamos el capítulo 17.6, donde habíamos creado una aplicación cliente/servidor con la siguiente arquitectura:

Ciertas acciones del usuario en la interfaz Swing en [1] desencadenan acciones que llegan hasta la base de datos en [3] a través de una red HTTP [2]. Debido a esto, la respuesta a la acción del usuario puede tardar más o menos tiempo en llegar. Sería conveniente poder incluir un indicador de espera en la interfaz de usuario con la opción de cancelar la operación iniciada si esta tardara demasiado. En el capítulo 17.6, cada acción del usuario que requiera intercambiar información con el servidor es síncrona. El manejador de eventos ejecutado por el código no finaliza hasta que se recibe la respuesta. Durante todo ese tiempo, la interfaz gráfica queda congelada: no responde a nuevas acciones del usuario. Estas acciones simplemente se ponen en cola para ser procesadas cuando termine el manejador de eventos que se está ejecutando en ese momento. Así, si se mostrara un botón de cancelación, el usuario podría hacer clic en él, pero no pasaría nada hasta que la operación en curso hubiera terminado. El botón de cancelación no tendría entonces ningún sentido.

Para que al hacer clic en el botón de cancelación se produzca el efecto deseado, es necesario que la operación en curso haya finalizado. Para ello, debe iniciar la operación que podría tardar mucho tiempo de manera asíncrona:

  • el controlador de eventos inicia la operación prolongada, pero no espera su resultado y devuelve el control al hilo de UI, que gestiona los eventos de la interfaz gráfica. La operación prolongada se inicia en un hilo diferente al de UI, lo que evita que este último se bloquee;
  • si el usuario hace clic en el botón de cancelación antes de que finalice la operación prolongada, el hilo de UI, que está libre, puede procesar este evento. De esta manera, se puede cancelar la operación prolongada sin tener en cuenta su resultado;
  • si la operación prolongada no se ha cancelado, la llegada de la respuesta provocará un evento en el hilo de UI. Este, si está libre, ejecutará entonces el código relacionado con dicho evento, el cual procesará la respuesta;

La interfaz de usuario funcionará como antes. Si los tiempos de respuesta del servidor son rápidos, el usuario no notará la diferencia. Si son perceptibles, al usuario le aparecerá un botón de cancelación y tendrá la posibilidad de interrumpir la operación en curso.

La biblioteca [Rx] permite realizar programación asíncrona. Su gran ventaja radica en que ha sido adaptada a numerosos entornos (Java, .NET, JS, ...) y que los conocimientos adquiridos en un entorno pueden trasladarse fácilmente a otro. Nos basaremos aquí en el capítulo 2 del documento [Introduction à RxJava. Application aux environnements Swing et Android]. Se invita al lector a leerlo. A continuación, retomamos código tomado de los ejemplos de ese capítulo.

Vamos a desarrollar la arquitectura de la aplicación de la siguiente manera:

  • en [1], intercalamos una capa [RxJava] entre la capa [swing] y la capa [métier]. Los métodos de esta última ahora se llamarán de manera asíncrona;

Procederemos en varias etapas:

  • paso 1: la capa [metier, DAO] presenta, por el momento, una interfaz síncrona con la capa [ui]. La transformaremos en una capa asíncrona [RxJava, metier, DAO];
  • paso 2: adaptaremos la aplicación de consola síncrona para que siga siendo síncrona, pero utilizando la interfaz asíncrona [RxJava, metier, DAO];
  • paso 3: convertiremos la aplicación Swing sincrónica en una aplicación Swing asincrónica;

20.1. paso 1

Transformamos la capa sincrónica actual [metier, DAO] en una capa asincrónica [RxJava, metier, DAO].

20.1.1. Creación

Partimos del proyecto Maven del capítulo 17.4, que abrimos con NetBeans:

Duplicamos este proyecto [1] (copiar/pegar) en un nuevo proyecto [elections-rxjava-metier-dao-security-webjson] [2].

20.1.2. Configuración de Maven

Modificamos el archivo [pom.xml] del nuevo proyecto para agregar la dependencia de la 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 por 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 por 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 registros -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <!-- Prueba de 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>
  • líneas 65-70: hemos agregado la dependencia de la biblioteca RxJava;

20.1.3. Implementación asíncrona de la capa [métier]

Para implementar la capa [RxJava, métier], agregamos una interfaz asíncrona [IRxElectionsMetier] [1] y su implementación [RxElectionsMetier] [2] al proyecto:

  

La interfaz [IRxElectionsMetier] es la interfaz asíncrona de la capa [RxJava, métier]. Su código es el siguiente:


package elections.security.client.metier;

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

public interface IRxElectionsMetier {

  // autenticación
  Observable<Void> authenticate(User user);

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

  // el número de escaños por cubrir
  Observable<Integer> getNbSiegesAPourvoir(User user);

  // el umbral electoral
  Observable<Double> getSeuilElectoral(User user);

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

  // el cálculo de escaños
  Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);

}

La interfaz [IRxElectionsMetier] retoma los métodos de la interfaz [IElectionsMetier], pero mientras que un método M de la interfaz [IElectionsMetier] devolvía un resultado de tipo T, el método M de la interfaz [IRxElectionsMetier] devuelve un resultado de tipo Observable<T>. El tipo [Observable] es proporcionado por la biblioteca RxJava. Un tipo Observable<T> proporciona el método [subscribe], que obtendrá el tipo T de manera asíncrona. A este método se le asocian tres eventos:

  • onSuccess(T result), que avisa de que hay un resultado de tipo T disponible. La operación asíncrona puede proporcionar varios resultados;
  • onError(Throwable th), que avisa de que la operación asíncrona ha encontrado un error;
  • onCompleted(), que notifica que la operación asincrónica ha finalizado;

Mientras no se llame al método [Observable.subscribe], la operación asíncrona vinculada al observable no se inicia. El código que invoca un método M de la interfaz [IRxElectionsMetier] no obtiene el resultado T esperado, sino un tipo Observable<T> que le permitirá posteriormente obtener el resultado T al invocar el método [Observable.subscribe].

La implementación [RxElectionsMetier] de la interfaz [IRxElectionsMetier] es la siguiente:


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 {
        // llamada al método síncrono y luego respuesta al suscriptor
        subscriber.onNext(metier.getListesElectorales(user));
        // se notifica el fin del observable
        subscriber.onCompleted();
      } catch (Exception e) {
        // se reenvía la excepción
        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) {
    ...
  }
}
  • líneas 12-13: inyección de Spring de la capa de negocio síncrona;
  • líneas 20-34: vamos a comentar el método [getListesElectorales], que en lugar de devolver un tipo [ListeElectorale[]] devuelve un tipo [Observable<ListeElectorale[]>];
  • líneas 22-32: el método estático [Observable.create] permite crear un Observable a partir de un tipo [Subscriber]. El tipo [Subscriber] representa un suscriptor de los flujos de resultados generados por el proceso observado (el Observable). Proporciona tres métodos:
    • [Subscriber.onNext] (línea 25) para recibir un resultado del proceso observado;
    • [Subscriber.onError] (línea 30) para recibir una excepción del proceso observado. Tras una excepción, el tipo [Observable] ya no emite resultados;
    • [Subscriber.onCompleted] (línea 27) para recibir la señal de fin de emisión del proceso observado. En este caso, el proceso observado solo emite un elemento. Cabe señalar que esta señal no se emite si ocurre una excepción. Este es el comportamiento predeterminado de los Observables: la emisión de una excepción también indica el fin de las emisiones. Los suscriptores lo saben;
  • líneas 22-34: el método [Observable.create] admite como parámetro un tipo [Observable.OnSubscribe]. Este tipo es una interfaz funcional. Este concepto se introdujo con Java 8 y se refiere a una interfaz que tiene un único método. En este caso, el único método de la interfaz [Observable.OnSubscribe] es el siguiente:
T call(Subscriber<T> subscriber)

Para implementar una interfaz funcional con un único método m(param1, param2, ..., paramn), se puede utilizar la siguiente sintaxis simplificada:

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

Esto es lo que se hace en las líneas 22 a 34:

  • [subscriber] es el parámetro del método [Observable.OnSubscribe.call];
  • líneas 23 a 32: el código que se le quiere asignar al método [call];
  • línea 25: se solicitan las listas electorales de manera sincrónica a la capa [métier] inyectada en la línea 13. Por lo tanto, habrá una espera hasta recibir el resultado. Cuando se reciba, se pasa al método [onNext] del suscriptor;
  • línea 28: en caso de error, la excepción se pasa al método [onError] del suscriptor;
  • línea 31: solo se espera un resultado. Una vez obtenido este (las listas electorales o una excepción), se le indica al suscriptor que el proceso observado ha terminado de generar resultados;

Hay que recordar que el método [RxElectionsMetier] devuelve un tipo Observable<ListeElectorale[]> y no el tipo ListeElectorale[] en sí. El código que realiza la llamada deberá invocar el método Observable<ListeElectorale[]>.subscribe para que se ejecute el código de las líneas 23 a 33 y se devuelvan las listas electorales mediante la línea 25.

El código de los demás métodos es similar:


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 {
        // llamada al método síncrono
        metier.authenticate(user);
        // se notifica el fin del observable
        subscriber.onCompleted();
      } catch (Exception e) {
        // se reenvía la excepción
        subscriber.onError(e);
      }
    });

  }

  @Override
  public Observable<ListeElectorale[]> getListesElectorales(User user) {
    return Observable.create(subscriber -> {
      try {
        // se llama al método síncrono y luego se responde al suscriptor
        subscriber.onNext(metier.getListesElectorales(user));
        // se notifica el fin del observable
        subscriber.onCompleted();
      } catch (Exception e) {
        // se reenvía la excepción
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Integer> getNbSiegesAPourvoir(User user) {
    return Observable.create(subscriber -> {
      try {
        // se llama al método síncrono y luego se responde al suscriptor
        subscriber.onNext(metier.getNbSiegesAPourvoir(user));
        // se notifica el fin del observable
        subscriber.onCompleted();
      } catch (Exception e) {
        // se reenvía la excepción
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Double> getSeuilElectoral(User user) {
    return Observable.create(subscriber -> {
      try {
        // se llama al método síncrono y luego se responde al suscriptor
        subscriber.onNext(metier.getSeuilElectoral(user));
        // se notifica el fin del observable
        subscriber.onCompleted();
      } catch (Exception e) {
        // se reenvía la excepción
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // se llama al método síncrono
        metier.recordResultats(user, listesElectorales);
        // se notifica el fin del observable
        subscriber.onCompleted();
      } catch (Exception e) {
        // se reenvía la excepción
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // se llama al método síncrono y luego se responde al suscriptor
        subscriber.onNext(metier.calculerSieges(user, listesElectorales));
        // se notifica el fin del observable
        subscriber.onCompleted();
      } catch (Exception e) {
        // se reenvía la excepción
        subscriber.onError(e);
      }
    });
  }
}
  • líneas 20 y 81: el método [onNext] del suscriptor no se invoca porque este no espera resultados;

20.1.4. Las pruebas JUnit de la capa [métier]

  

20.1.4.1. Test01

Retomamos la prueba unitaria [Test01] analizada en el párrafo 17.4.4. Fue diseñada para realizar llamadas síncronas a la interfaz [IElectionsMetier]. Lo modificamos para que realice llamadas sincrónicas a la nueva interfaz [IRxElectionsMetier]. De hecho, es posible realizar llamadas sincrónicas a una interfaz asincrónica RxJava. El código queda así:


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 {

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

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

  // usuarios
  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() {
    // se crea la tabla de las 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);
    // se calculan los escaños de cada una de las listas
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // se verifican los 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() {
    // se crea la tabla de las 7 listas de candidatos
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // se fijan los 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);
    // se calculan los escaños obtenidos por cada una de las listas
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // se verifican los 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() {
    // se crea una tabla con 24 listas candidatas, cada una con 1 voto
    ListeElectorale[] listes = new ListeElectorale[25];
    // las 25 listas tendrán el mismo 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 de escaños: normalmente se debe obtener un ElectionsException
    // con un umbral electoral del 5 %
    BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
  }

  /**
   * enregistrement des résultats de l'élection
   *
   * @throws JsonProcessingException
   */
  @Test
  public void ecritureResultatsElections() throws JsonProcessingException {
    // se crea la tabla de las 7 listas candidatas
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // se fijan los 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);
    // se calculan los escaños obtenidos por cada una de las listas
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // se muestran los resultados
    for (int i = 0; i < listes.length; i++) {
      System.out.println(mapper.writeValueAsString(listes[i]));
    }
    // se guardan los resultados en la base de datos
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
    // se verifican los resultados
    listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // se muestran los 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());
  }
}

Analicemos los cambios:

  • línea 48: el método estático [BlockingObservable.from(Observable).first]:
    • se suscribe al parámetro observable de [from];
    • inicia la ejecución del código asociado al observable;
    • espera a recibir el primer resultado. Por lo tanto, se trata de una operación sincrónica;

Aquí utilizamos el método [firstOrDefault(null)] porque el observable [metier.authenticate] no devuelve ningún resultado cuando se ejecuta. El resultado del método [firstOrDefault(null)] será, por lo tanto, null, un valor que no se utiliza aquí;

Repetimos este esquema en el resto del código cada vez que queremos llamar a la capa [métier].

La prueba unitaria [Test01] debe pasar:

 

Tarea pendiente: verificar que la prueba [Test01] sea exitosa.


20.1.4.2. Test02

Modificamos la prueba [Test01] para que ahora pruebe la interfaz asíncrona [IRxElectionsMetier] mediante llamadas asíncronas a sus métodos.

Analicemos una primera prueba:


  // semáforo de sincronización de subprocesos
  private CountDownLatch latch;

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

  @Test()
  public void checkUserUser() throws InterruptedException {
    // semáforo en 1
    latch = new CountDownLatch((1));
    // operación asíncrona
    electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // espera de semáforo
    latch.await();
    // verificación de resultados
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • línea 2: un semáforo es una herramienta que se utiliza para sincronizar hilos entre sí. Los hilos son flujos de ejecución que se ejecutan en paralelo. Para ejecutar una tarea T1, el hilo [Thread1] puede necesitar que una tarea T2, ejecutada por un hilo [Thread2], haya finalizado. Por lo tanto, espera a que el hilo [Thread2] le envíe una señal indicando que la tarea T2 ha finalizado. Existen diversas formas de gestionar esta sincronización entre dos hilos. El método utilizado aquí es el siguiente:
    • línea 10: el hilo [Thread1] crea un semáforo con el valor 1;
    • línea 12: el hilo [Thread1] crea e inicia un hilo [Thread2]. Esto se logra mediante la sintaxis:

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

El método [Observable.subscribeOn] establece el hilo en el que se ejecutará el proceso observado. El parámetro de [subscribeOn] es un grupo de subprocesos. La biblioteca RxJava ofrece varios grupos adaptados a diferentes situaciones. El grupo [Schedulers.io()] es el recomendado para operaciones de red;

  • (continuación)
    • líneas 12-13: la operación

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

ejecuta la operación síncrona encapsulada en el observable [authenticate(user)]. Pero como esta operación síncrona se inicia en un hilo distinto al hilo [Thread1], este último no espera la respuesta del método [subscribe] y pasa a la instrucción siguiente;

  • (continuación)
    • línea 23: el hilo [Thread1] se detiene y espera a que el semáforo pase a 0 (por el momento está en 1);
  • líneas 13-21: el método [subscribe] admite como parámetros tres funciones lambda:
    • la primera, [(result)->{...}], se invoca cada vez que el observable [authenticate(user)] emite un resultado [result]. Aquí tenemos un observable [authenticate(user)] que hace algo, pero no emite ningún resultado. Por lo tanto, la función lambda [(result)->{}] nunca se llamará. Por eso su código está vacío aquí: [{}];
    • la segunda, [(th)->{...}], recibe como parámetro un tipo [Throwable]. Se invoca cuando la ejecución del observable encuentra una excepción. Aquí, procesamos el parámetro [Throwable th] de la siguiente manera:
      • línea 16: lo almacenamos en un campo de la clase de prueba de tipo [ElectionsException], ya que el observable ejecutado solo genera este tipo de excepción;
      • línea 17: ponemos el semáforo a 0 para indicar que el hilo [Thread2] ha terminado su trabajo;
    • el tercer observable, [()->{...}], se invoca cuando el observable ya no tiene elementos que emitir. Procesamos este evento de la siguiente manera:
      • línea 20: ponemos el semáforo a 0 para indicar que el hilo [Thread2] ha terminado su trabajo;

Cabe señalar que la tercera función lambda no se invoca si se produce una excepción. Por eso, nos vimos obligados a poner el semáforo a 0 también en la línea 17;

  • línea 25: cuando llegamos a esta línea, el observable ha terminado su trabajo. Entonces podemos realizar las mismas verificaciones que en la prueba [Test01];

Analicemos otra prueba:


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

  @Test
  public void calculSieges1() throws InterruptedException {
    // se crea la matriz de las 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 en 1
    latch = new CountDownLatch((1));
    // operación asíncrona    
    // se calculan los escaños de cada una de las listas
    electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // espera de semáforo
    latch.await();
    // se verifican los 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());
  }
  • líneas 20-30: ejecución asíncrona del observable [electionsMetier.calculerSieges(admin, listes)];
  • líneas 21-23: la ejecución del observable devuelve un tipo [ListeElectorale[]] que se almacena en un campo de la clase de prueba, línea 3;
  • líneas 34-48: estas verificaciones son las de la prueba [Test01], a las que se ha agregado la verificación de la línea 34, que garantiza que no haya habido ninguna excepción;

La prueba completa [Test02] está disponible en el material del curso.


Tarea: ejecutar la prueba [Test02] y verificar que se ejecute correctamente.


20.1.4.3. Test03

La prueba [Test03] hace lo mismo que la prueba [Test01]: prueba la interfaz [IRxElectionsMetier] mediante llamadas sincrónicas a dicha interfaz. Es una copia de la prueba [Test02] con dos diferencias:

  • los observables ya no se ejecutan en un hilo diferente al que ejecuta las pruebas. Cuando el hilo [Thread1] ejecuta el método [subscribe] de un observable, este inicia una operación HTTP hacia el servidor, también en el hilo [Thread1]. Todo el método [subscribe] se vuelve entonces síncrono;
  • dado que ya solo hay un hilo, la sincronización de hilos deja de ser necesaria y el semáforo desaparece;

A continuación se presentan dos ejemplos de pruebas:


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

  @Test()
  public void checkUserUser() throws InterruptedException {
    // operación síncrona
    electionsMetier.authenticate(user)
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                    },
                    () -> {
                    });
    // verificación de resultados
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • línea 7: por defecto, el método [electionsMetier.authenticate(user).subscribe] se ejecuta en el hilo del código que lo llama. Por lo tanto, se trata de una operación síncrona;

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

  @Test
  public void calculSieges1() throws InterruptedException {
    // se crea la tabla de las 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);
    // operación síncrona    
    // se calculan los escaños de cada una de las listas
    electionsMetier.calculerSieges(admin, listes)
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                    },
                    () -> {
                    });
    // se verifican los 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());
  }

Tarea a realizar: ejecutar la prueba [Test03] y verificar que pase.


20.2. paso 2

Ahora vamos a convertir la aplicación de consola síncrona del capítulo 17.5 en una aplicación que siga siendo síncrona, pero que utilice la interfaz asíncrona [RxJava, metier, DAO];

Partimos del proyecto [elections-console-metier-dao-security-webjson] [1] del capítulo 17.5, el cual duplicamos en un nuevo proyecto [elections-console-rxjava- metier-dao-security-webjson] [2]:

  • en [3-4]; en el nuevo proyecto eliminamos la dependencia de la antigua capa síncrona [métier];
  • en [5-9], se agrega una dependencia de la nueva capa asíncrona [métier];
  • en [10-14], se cambia el nombre de la clase [ElectionsConsole] a [ElectionsConsole01];

Del mismo modo, se cambia el nombre de la clase [BootElectionsConsole] a [BootElectionsConsole01]:

 

El código actual de la clase [BootElectionsConsole01] es el siguiente:


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);
    }
}
  • línea 13: dado que se ha cambiado el nombre de la clase [ElectionsConsole] a [ElectionsConsole01], ahora hay que escribir:

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

Volvamos al código de la clase [ElectionsConsole01]:


@Component
public class ElectionsConsole01 implements IElectionsUI {

    @Autowired
    private IElectionsMetier electionsMetier;

  @Autowired
  private User admin;
  
    @Override
    public void run() {
        // las listas en contienda
        ListeElectorale[] listes;
        // ingreso de datos
        try (Scanner clavier = new Scanner(System.in)) {
         // se solicitan las listas en contienda a la capa [metier]
         listes = electionsMetier.getListesElectorales(admin);
            ...
        // se realiza el cálculo de escaños
        listes=electionsMetier.calculerSieges(admin,listes);
        // se registran los resultados
        electionsMetier.recordResultats(admin,listes);
        ...
}

Si seguimos el ejemplo de la prueba [Test01] del párrafo 20.1.4.1, las líneas 5, 17, 20 y 22 cambiarán de la siguiente manera:


@Component
public class ElectionsConsole01 implements IElectionsUI {

  @Autowired
  private IRxElectionsMetier electionsMetier;

  @Autowired
  private User admin;

  @Override
  public void run() {
    // las listas en competencia
    ListeElectorale[] listes;
    // ingreso de datos
    try (Scanner clavier = new Scanner(System.in)) {
      // se solicitan las listas en competencia a la capa [metier]
      listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
      ...
    // se realiza el cálculo de escaños
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // se registran los resultados
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
    ...
  }

Tarea: configura el proyecto para ejecutar la clase [BootElectionsConsole01] con los tres parámetros [SS, Heures travaillées, Jours travaillés] y verifica que la ejecución del proyecto así configurado arroje los resultados esperados.



Tarea: configure el proyecto para ejecutar el par [BootElectionsConsole02, ElectionsConsole02], donde la clase [ElectionsConsole02] se habrá escrito siguiendo el modelo de la prueba [Test02] del párrafo 20.1.4.2.



Tarea: configure el proyecto para ejecutar el par [BootElectionsConsole03, ElectionsConsole03], donde la clase [ElectionsConsole03] se habrá escrito siguiendo el modelo de la prueba [Test03] del párrafo 20.1.4.3.


20.3. Paso 3

Ahora pasamos a la adaptación de la aplicación Swing a un entorno asíncrono.

Comenzamos duplicando el proyecto [elections-swing-metier-dao-security-webjson] [1] del capítulo 17.6, en un nuevo proyecto [elections-swing-rxjava-metier-dao-security-webjson] [2]:

  • en [3, 4], eliminamos la dependencia de la capa síncrona [console];
  • en [5-9], agregamos una dependencia de la capa de consola asíncrona;

La capa [swing] realizará llamadas asincrónicas reales a la capa [métier]. Al llamar a un método de esta última, habrá dos hilos:

  • el hilo de UI, que gestiona los eventos;
  • un hilo de E/S que ejecutará la llamada HTTP al servidor;

Durante toda la duración de la llamada asíncrona, deberíamos mostrar una imagen de espera y un botón de cancelación. No lo haremos aquí y esto se te propondrá como una mejora de la aplicación. Las modificaciones se realizan en las dos clases que realizan llamadas a la capa [métier]:

 

20.3.1. Configuración de Maven

Aquí utilizaremos la biblioteca [RxSwing], que agrega a la biblioteca [RxJava] funcionalidades disponibles únicamente en un entorno Swing. Para ello, modificamos el archivo [pom.xml] de la siguiente manera:


<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>
    <!-- capas 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. La clase [ElectionsConnectForm]

En un funcionamiento asíncrono, la clase [ElectionsConnectForm] queda así:


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;

  // referencia en la capa [métier] asíncrona
  @Autowired
  private IRxElectionsMetier metier;

  // usuario conectado
  private User user;

  // formulario principal
  @Autowired
  private ElectionsMainForm electionsMainForm;

  // sesión UI
  @Autowired
  private UiSession uiSession;

  @Override
  protected void doConnect() {
    if (isPageValid()) {
      // autenticación del usuario
      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
        // no hay respuesta
        (result) -> {
        },
        // manejo de la excepción
        (th) -> {
          // se registra el error
          String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
          // se muestra la información
          jTextPaneErreurs.setText(info);
          jTextPaneErreurs.setCaretPosition(0);

        },
        // la autenticación ha finalizado
        () -> {
          // se guarda el usuario en la sesión
          uiSession.setUser(user);
          // se oculta la pantalla de inicio de sesión
          setVisible(false);
          // se muestra la vista principal
          electionsMainForm.run();
        });
    }
  }

  // inicializaciones
  @Override
  protected void init() {
    ...
  }

  @Override
  public void run() {
    // se muestra la interfaz gráfica
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
  }

  private boolean isPageValid() {
    ...
  }

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

}
  • líneas 36-63: el método [doConnect] se ejecuta cuando el usuario presiona la opción de menú [Connexion]:
 

Todo está en la línea 40:


      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
  • el proceso observado es [metier.authenticate(user)];
  • se ejecutará en un hilo de E/S tomado del grupo [Schedulers.io()];
  • se observará en el hilo de UI, el que gestiona los eventos de la interfaz Swing [observeOn(SwingScheduler.getInstance())]. Este hilo se obtiene mediante el método [SwingScheduler.getInstance()], donde [SwingScheduler] es una clase proporcionada por la biblioteca [RxSwing]. Esto es obligatorio. Al obtener el resultado de la operación asíncrona, este se utiliza con frecuencia para modificar elementos de la interfaz Swing. Sin embargo, esta solo puede modificarse en el hilo de UI; de lo contrario, se produce una excepción. Por lo tanto, las líneas 41 a 61 deben ejecutarse en el hilo de UI. Esto se garantiza aquí mediante el método [observeOn(SwingScheduler.getInstance())];

Comentemos el resto del código:

  • líneas 42-43: estas líneas están ahí para cumplir con la sintaxis del método [subscribe]. Nunca se ejecutarán porque el proceso [metier.authenticate(user)] no devuelve ningún resultado;
  • líneas 35-52: al recibir una excepción, esta se muestra;
  • líneas 54-61: se ejecutan cuando el proceso [metier.authenticate(user)] indica que ha finalizado sus transmisiones;

20.3.3. La clase [ElectionsMainForm]

 

20.3.3.1. Inicialización de la interfaz 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;

  // referencia a la capa asincrónica [métier]
  @Autowired
  private IRxElectionsMetier metier;

  // Sesión UI
  @Autowired
  private UiSession uiSession;

  // usuario conectado
  private User user;

  // plantillas de las listas JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // las listas en competencia
  private ListeElectorale[] listes;

  // listas ingresadas por el usuario
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

  // inicializaciones
  @Override
  protected void init() {
    // generación de componentes por la clase padre
    super.init();
    // estado del formulario
    Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
    Utilitaires.setEnabled(
            new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
    // centrar la ventana
    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);
    // usuario conectado
    user = uiSession.getUser();
    // inicializaciones locales
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    // se solicitan las listas a la capa [métier]
    metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // respuesta
                    listesElectorales -> {
                      // se almacenan las listas
                      listes = listesElectorales;
                    },
                    // excepción
                    (th) -> showException(th),
                    // fin observable
                    () -> {
                      // siguiente paso
                      doInitStep2();
                    });
  }
...
  • línea 46: el método [init] se ejecuta cuando se va a mostrar la ventana asociada. Su objetivo es inicializar los componentes [1-3] que se indican a continuación:
 
  • líneas 71-85: se solicitan de manera asíncrona las listas de candidatos (componente [1]);
  • línea 71: el proceso observado es [metier.getListesElectorales(user)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
  • líneas 74-77: el resultado devuelto por el proceso observado se almacena en el campo [listes] de la línea 38;
  • línea 79: cualquier excepción se maneja mediante el siguiente método:

  private void showException(Throwable th) {
    // se muestra la excepción
    jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
    jTextPaneMessages.setCaretPosition(0);
}
  • líneas 81-84: al finalizar el proceso observado, se ejecutan las líneas 81-84. Estas líneas no se ejecutan si se ha producido una excepción. El método [doInitStep2] lleva a cabo el paso 2 de la inicialización de la siguiente manera:

  private void doInitStep2() {
    // se asocian los nombres de las listas al menú desplegable jComboBoxNomsListes
    for (int i = 0; i < listes.length; i++) {
      jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
    }
    // número de puestos vacantes
    metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // respuesta
                    nbSiegesAPourvoir -> {
                      // se inicializa la etiqueta vinculada a esta información
                      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
                    },
                    // excepción
                    (th) -> showException(th),
                    // fin observable
                    () -> {
                      // siguiente paso
                      doInitStep3();
                    });
}
  • líneas 3-5: se utiliza el resultado del paso anterior para completar la lista desplegable con los nombres de las listas de candidatos;
  • líneas 7-20: se solicita el número de puestos vacantes de manera asíncrona;
  • línea 7: el proceso observado es [metier.getNbSiegesAPourvoir(user)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
  • líneas 10-13: el resultado devuelto por el proceso se utiliza para actualizar la interfaz gráfica;
  • línea 15: se muestra cualquier excepción que pueda surgir;
  • líneas 17-20: al recibir la señal de fin del observable, se pasa al paso 3 del proceso de inicialización;

El paso 3 de la inicialización se lleva a cabo mediante el siguiente código:


  private void doInitStep3() {
    // umbral electoral
    metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // respuesta
                    seuilElectoral -> {
                      // se inicializa la etiqueta vinculada a esta información
                      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
                    },
                    // excepción
                    (th) -> showException(th),
                    // fin observable
                    () -> {
                    });
}
  • líneas 3-4: se solicita el umbral electoral de manera asíncrona;
  • línea 3: el proceso observado es [metier.getSeuilElectoral(user)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
  • líneas 6-9: el resultado devuelto por el proceso se utiliza para actualizar la interfaz gráfica;
  • línea 11: se muestra cualquier excepción que pueda surgir;
  • líneas 13-14: al recibir la señal de fin del observable, no se realiza ninguna acción: el proceso de inicialización de la interfaz gráfica ha finalizado;

20.3.3.2. Cálculo de los escaños obtenidos por las diferentes listas

El método [doCalculer] tiene como función calcular el número de escaños obtenidos por las distintas listas:


  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // cálculo de escaños
    String info = null;
    metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // procesamiento del resultado
                    result -> consumeResultSieges(result),
                    // procesamiento de la excepción
                    th -> showException(th),
                    // fin observable
                    () -> {
                    }
            );
}
  • líneas 6-15: se calculan de manera asíncrona los escaños obtenidos por las distintas listas;
  • línea 6: el proceso observado es [metier.calculerSieges(user, tListesSaisies)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
  • línea 9: el resultado devuelto por el proceso es utilizado por el método [consumeResultSieges];
  • línea 11: se muestra la excepción, si la hay;
  • líneas 13-14: al recibir la señal de fin del observable, no se realiza ninguna acción;

Línea 9: el método [consumeResultSieges] procesa el resultado devuelto por el proceso observado, las listas de candidatos con sus campos [sieges, elimine] actualizados:


  private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
    // se guarda el resultado
    this.tListesSaisies = tListesSaisies;
    // visualización de los resultados
    modèleRésultats.clear();
    for (int i = 0; i < tListesSaisies.length; i++) {
      modèleRésultats.addElement(tListesSaisies[i].toString());
    }
    // actualización del estado del formulario
    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é");
}
  • líneas 4-14: el resultado obtenido se utiliza para actualizar la interfaz gráfica;

20.3.3.3. Registro de los resultados de la elección

El registro de los resultados de la elección se realiza mediante el siguiente método [doEnregistrer]:


  @Override
  protected void doEnregistrer() {
    // se solicita el registro a la capa [métier]
    metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // procesamiento del resultado: aquí no hay ninguno
                    (param) -> {
                    },
                    // procesamiento de la excepción
                    (th) -> showException(th),
                    // fin observable
                    () -> {
                      // actualización del formulario
                      Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
                      Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
                      jTextPaneMessages.setText("Enregistrement des résultats réalisé");
                    }
            );
}
  • líneas 4-17: se registran los resultados de la elección de manera asíncrona;
  • línea 4: el proceso observado es [metier.recordResultats(user, tListesSaisies)]. Se ejecuta en un hilo de E/S [subscribeOn(Schedulers.io())] y se observa en el hilo de UI [observeOn(SwingScheduler.getInstance()];
  • líneas 7-8: estas líneas nunca se ejecutarán porque el proceso observado no devuelve ningún resultado;
  • línea 10: se muestra la posible excepción;
  • líneas 14-16: al recibir la señal de fin del observable, se actualiza la interfaz gráfica;

Tarea a realizar: verifica que la aplicación Swing funcione. Luego, modifica la interfaz gráfica y el código para que, durante una operación asíncrona con el servidor web / jSON, aparezca una imagen de espera y una opción para cancelar la operación en curso.