Skip to content

20. Programación asíncrona con RxJava

Documento de lectura: [Introduction à RxJava. Application aux environnements Swing et Android.]

En este capítulo, volvemos al capítulo 17.6, donde creamos una aplicación cliente/servidor con la siguiente arquitectura:

Algunas acciones del usuario en la interfaz Swing de [1] desencadenan acciones que llegan hasta la base de datos de [3] a través de una red HTTP [2]. Debido a ello, 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 una opción para cancelar la operación iniciada si esta se alargara demasiado. En el capítulo 17.6, cada acción del usuario que requiera intercambiar información con el servidor es sincrónica. El gestor de eventos ejecutado por el código no finaliza hasta que se recibe la respuesta. Durante todo ese tiempo, la interfaz gráfica queda bloqueada: no responde a nuevas acciones del usuario. Estas acciones simplemente se ponen en cola para ser procesadas cuando el gestor de eventos que se está ejecutando en ese momento haya finalizado. 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 finalizado. El botón de cancelación no tendría entonces ninguna utilidad.

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 puede ser larga, de forma asíncrona:

  • el gestor 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 gestionar este evento. De este modo, se puede abandonar la operación prolongada ignorando 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 asociado a dicho evento, que procesará la respuesta;

La interfaz de usuario funcionará como hasta ahora. 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 interés radica en que se ha adaptado 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 recomienda al lector que lo lea. A continuación, retomamos código procedente de los ejemplos de dicho 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]. A partir de ahora, los métodos de esta última se llamarán de forma 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 síncrona en una aplicación Swing asíncrona;

20.1. paso 1

Transformamos la capa sincrónica actual [metier, DAO] en una capa asíncrona [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 añadir 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>
    <!-- Spring Boot Test -->
    <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 añadido 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], añadimos 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);

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

}

La interfaz [IRxElectionsMetier] recoge 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] lo proporciona la biblioteca RxJava. Un tipo Observable<T> proporciona el método [subscribe], que obtendrá el tipo T de forma 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 detectado un error;
  • onCompleted(), que notifica que la operación asíncrona 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 posterior respuesta al suscriptor
        subscriber.onNext(metier.getListesElectorales(user));
        // se notifica el fin de la observación
        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 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] deja de emitir 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 se produce una excepción. Este es el comportamiento por defecto 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 hace referencia 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-34:

  • [subscriber] es el parámetro del método [Observable.OnSubscribe.call];
  • líneas 23-32: el código que se quiere asignar al método [call];
  • línea 25: se solicitan las listas electorales de forma sincrónica a la capa [métier] inyectada en la línea 13. Por lo tanto, habrá que esperar a 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. Cuando se haya obtenido (las listas electorales o una excepción), se indica al suscriptor que el proceso observado ha terminado de emitir resultados;

Hay que tener muy presente que el método [RxElectionsMetier] devuelve un tipo Observable<ListeElectorale[]> y no el tipo ListeElectorale[] en sí mismo. 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-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, a continuación, 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, a continuación, 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, a continuación, 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 {
        // llamada 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, a continuación, 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 apartado 17.4.4. Se diseñó 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 asíncrona 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 comprueban 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 comprueban 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 debería salir 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 de forma definitiva
    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 comprueban 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 tanto, null, un valor que aquí no se aprovecha;

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

La prueba unitaria [Test01] debe superarse:

 

Tarea pendiente: comprobar que la prueba [Test01] se supere.


20.1.4.2. Test02

Modificamos la prueba [Test01] para que ahora pruebe la interfaz asíncrona [IRxElectionsMetier] realizando 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 a 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 finalice una tarea T2 ejecutada por un hilo [Thread2]. 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 consigue 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 proporciona 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, dado que 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 realiza alguna acción, pero no emite ningún resultado. Por lo tanto, la lambda [(result)->{}] nunca se invocará. 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í, tratamos 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 lanza 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. Tratamos 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 hemos visto 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 comprobaciones 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 a 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 comprueban 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 añadido la verificación de la línea 34, que garantiza que no se ha producido ninguna excepción;

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


Tarea: ejecutar la prueba [Test02] y comprobar que se supera.


20.1.4.3. Test03

La prueba [Test03] hace lo mismo que la prueba [Test01]: comprueba 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 distinto 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] pasa entonces a ser síncrono;
  • dado que solo queda un hilo, la sincronización de hilos deja de ser necesaria y el semáforo desaparece;

A continuación se muestran 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 invoca. 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 comprueban 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 pendiente: pasar la prueba [Test03] y comprobar que se supera.


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, que 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 añade 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 renombra 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] por [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 liza
        ListeElectorale[] listes;
        // introducción de datos
        try (Scanner clavier = new Scanner(System.in)) {
         // se solicitan las listas en liza 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 apartado 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 liza
    ListeElectorale[] listes;
    // Introducción de datos
    try (Scanner clavier = new Scanner(System.in)) {
      // se solicitan las listas en liza 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 comprueba que la ejecución del proyecto así configurado da los resultados esperados.



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



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


20.3. Paso 3

Pasamos ahora 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], añadimos una dependencia de la capa de consola asíncrona;

La capa [swing] realizará llamadas asíncronas reales a la capa [métier]. Al llamar a un método de esta última, habrá dos subprocesos:

  • 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í, pero se te propondrá como 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í vamos a utilizar la biblioteca [RxSwing], que añade 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 de la siguiente manera:


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) -> {
        },
        // gestión 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 pulsa 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 suele utilizar para modificar elementos de la interfaz Swing. Sin embargo, esta solo se puede modificar en el hilo de UI; de lo contrario, se produce una excepción. Por lo tanto, las líneas 41-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 respetar la sintaxis del método [subscribe]. Nunca se ejecutarán, ya que 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)] señala el final de sus emisiones;

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 asíncrona [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 competición
  private ListeElectorale[] listes;

  // listas introducidas 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 forma 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 gestiona 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 cuadro combinado jComboBoxNomsListes
    for (int i = 0; i < listes.length; i++) {
      jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
    }
    // número de plazas disponibles
    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 rellenar la lista desplegable con los nombres de las listas de candidatos;
  • líneas 7-20: se solicita el número de escaños por cubrir de forma 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 la posible excepción;
  • líneas 17-20: al recibir la señal de fin del observable, se pasa a la etapa 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 de la observación
                    () -> {
                    });
}
  • líneas 3-4: se solicita el umbral electoral de forma 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 la posible excepción;
  • 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),
                    // tratamiento de la excepción
                    th -> showException(th),
                    // fin observable
                    () -> {
                    }
            );
}
  • líneas 6-15: se calculan de forma 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 posible excepción;
  • 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) -> {
                    },
                    // tratamiento 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 forma 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, ya que 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 pendiente: comprueba que la aplicación Swing funcione. A continuación, 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.