Skip to content

20. Programowanie asynchroniczne z wykorzystaniem RxJava

Dokument do przeczytania: [Introduction à RxJava. Application aux environnements Swing et Android.]

W niniejszym rozdziale powracamy do rozdziału 17.6, w którym stworzyliśmy aplikację klient-serwer o następującej architekturze:

Niektóre działania użytkownika w interfejsie Swing w [1] wyzwalają działania sięgające aż do bazy danych w [3] poprzez sieć HTTP [2]. Z tego powodu odpowiedź na działanie użytkownika może pojawić się z mniejszym lub większym opóźnieniem. Dobrze byłoby umieścić na interfejsie użytkownika wskaźnik oczekiwania wraz z opcją anulowania rozpoczętej operacji, gdyby trwała ona zbyt długo. W rozdziale 17.6 każda akcja użytkownika wymagająca wymiany informacji z serwerem jest synchroniczna. Obsługa zdarzenia wykonywana przez kod kończy się dopiero po otrzymaniu odpowiedzi. Przez cały ten czas interfejs graficzny jest zawieszony: nie reaguje na nowe działania użytkownika. Działania te są po prostu umieszczane w kolejce, aby zostały przetworzone po zakończeniu aktualnie wykonywanego obsługi zdarzeń. Tak więc, gdyby wyświetlono przycisk anulowania, użytkownik mógłby go kliknąć, ale nic by się nie stało, dopóki trwająca operacja nie zostałaby zakończona. Przycisk anulowania nie miałby wówczas żadnego sensu.

Aby kliknięcie przycisku anulowania przyniosło efekt, bieżąca operacja musi zostać zakończona. W tym celu musi ona uruchomić potencjalnie długotrwałą operację w trybie asynchronicznym:

  • obsługa zdarzenia uruchamia długotrwałą operację, ale nie czeka na jej wynik i przekazuje kontrolę wątkowi UI, który zarządza zdarzeniami interfejsu graficznego. Długotrwała operacja jest uruchamiana w wątku innym niż wątek UI, co nie blokuje tego ostatniego;
  • jeśli użytkownik kliknie przycisk anulowania przed zakończeniem długotrwałej operacji, wątek UI, który jest w tym momencie wolny, może obsłużyć to zdarzenie. Można wówczas przerwać długotrwałą operację, ignorując jej wynik;
  • jeśli długotrwała operacja nie została anulowana, nadejście odpowiedzi wywoła zdarzenie w wątku UI. Jeśli wątek ten jest wolny, wykona kod powiązany z tym zdarzeniem, który przetworzy odpowiedź;

Interfejs użytkownika będzie działał tak jak dotychczas. Jeśli czas odpowiedzi serwera jest krótki, użytkownik nie zauważy różnicy. Jeśli jednak opóźnienie jest zauważalne, użytkownik zobaczy przycisk anulowania i będzie miał możliwość przerwania trwającej operacji.

Biblioteka [Rx] umożliwia programowanie asynchroniczne. Jej wielką zaletą jest to, że została przeniesiona do wielu środowisk (Java, .NET, JS, ...) oraz że wiedza zdobyta w jednym środowisku może być łatwo przeniesiona do innego. Będziemy się tutaj opierać na rozdziale 2 dokumentu [Introduction à RxJava. Application aux environnements Swing et Android]. Zachęcamy czytelnika do zapoznania się z nim. W dalszej części wykorzystamy kod pochodzący z przykładów zawartych w tym rozdziale.

Będziemy rozwijać architekturę aplikacji w następujący sposób:

  • w [1] wstawiamy warstwę [RxJava] pomiędzy warstwą [swing] a warstwą [métier]. Metody tej warstwy będą odtąd wywoływane asynchronicznie;

Postępujemy w kilku etapach:

  • etap 1: warstwa [metier, DAO] posiada obecnie interfejs synchroniczny względem warstwy [ui]. Przekształcimy ją w warstwę asynchroniczną [RxJava, metier, DAO];
  • etap 2: przekształcimy synchroniczną aplikację konsolową w aplikację nadal synchroniczną, ale korzystającą z asynchronicznego interfejsu [RxJava, metier, DAO];
  • krok 3: przekształcimy synchroniczną aplikację Swing w asynchroniczną aplikację Swing;

20.1. krok 1

Przekształcamy obecną warstwę synchroniczną [metier, DAO] w warstwę asynchroniczną [RxJava, metier, DAO].

20.1.1. Tworzenie

Wychodzimy od projektu Maven z rozdziału 17.4, który otwieramy w programie NetBeans:

Duplikujemy ten projekt [1] (kopiuj/wklej) do nowego projektu [elections-rxjava-metier-dao-security-webjson] [2].

20.1.2. Konfiguracja Maven

Modyfikujemy plik [pom.xml] w nowym projekcie, aby dodać zależność od biblioteki [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>
    <!-- biblioteka jSON wykorzystywana przez 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>
    <!-- komponent używany przez 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>
    <!-- biblioteka logów -->
    <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>
  • wiersze 65–70: dodaliśmy zależność od biblioteki RxJava;

20.1.3. Asynchroniczna implementacja warstwy [métier]

Aby zaimplementować warstwę [RxJava, métier], dodajemy do projektu asynchroniczny interfejs [IRxElectionsMetier] [1] oraz jego implementację [RxElectionsMetier] [2]:

  

Interfejs [IRxElectionsMetier] jest interfejsem asynchronicznym warstwy [RxJava, métier]. Jego kod wygląda następująco:


package elections.security.client.metier;

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

public interface IRxElectionsMetier {

  // uwierzytelnianie
  Observable<Void> authenticate(User user);

  // pobierz listy kandydatów
  Observable<ListeElectorale[]> getListesElectorales(User user);

  // liczba mandatów do obsadzenia
  Observable<Integer> getNbSiegesAPourvoir(User user);

  // próg wyborczy
  Observable<Double> getSeuilElectoral(User user);

  // rejestrowanie wyników
  Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);

  // obliczanie liczby mandatów
  Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);

}

Interfejs [IRxElectionsMetier] przejmuje metody z interfejsu [IElectionsMetier], ale tam, gdzie metoda M z interfejsu [IElectionsMetier] zwracała wynik typu T, metoda M interfejsu [IRxElectionsMetier] zwraca wynik typu Observable<T>. Typ [Observable] jest dostarczany przez bibliotekę RxJava. Typ Observable<T> udostępnia metodę [subscribe], która asynchronicznie pobiera typ T. Z tą metodą powiązane są trzy zdarzenia:

  • onSuccess(T result), które informuje, że dostępny jest wynik typu T. Operacja asynchroniczna może zwrócić wiele wyników;
  • onError(Throwable th), które informuje, że w operacji asynchronicznej wystąpił błąd;
  • onCompleted(), które informuje, że operacja asynchroniczna została zakończona;

Dopóki metoda [Observable.subscribe] nie zostanie wywołana, operacja asynchroniczna powiązana z obserwowalnym nie zostanie uruchomiona. Kod wywołujący metodę M interfejsu [IRxElectionsMetier] nie otrzymuje oczekiwanego wyniku T, lecz typ Observable<T>, który pozwoli mu później uzyskać wynik T poprzez wywołanie metody [Observable.subscribe].

Implementacja [RxElectionsMetier] interfejsu [IRxElectionsMetier] wygląda następująco:


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 {
        // wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
        subscriber.onNext(metier.getListesElectorales(user));
        // zgłaszamy koniec obserwowalnej wartości
        subscriber.onCompleted();
      } catch (Exception e) {
        // przekazywanie wyjątku
        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) {
    ...
  }
}
  • wiersze 12–13: wstrzyknięcie warstwy biznesowej przez Spring w trybie synchronicznym;
  • wiersze 20–34: skomentujemy metodę [getListesElectorales], która zamiast zwracać typ [ListeElectorale[]] zwraca typ [Observable<ListeElectorale[]>];
  • wiersze 22–32: statyczna metoda [Observable.create] pozwala utworzyć obiekt typu Observable na podstawie typu [Subscriber]. Typ [Subscriber] reprezentuje subskrybenta strumieni wyników generowanych przez obserwowany proces (Observable). Udostępnia on trzy metody:
    • [Subscriber.onNext] (wiersz 25) do odbierania wyniku z obserwowanego procesu;
    • [Subscriber.onError] (wiersz 30) do odbierania wyjątku z obserwowanego procesu. Po wystąpieniu wyjątku typ [Observable] nie wysyła już wyników;
    • [Subscriber.onCompleted] (wiersz 27) do odbierania sygnału zakończenia emisji z obserwowanego procesu. W tym przypadku obserwowany proces emituje tylko jeden element. Należy zauważyć, że sygnał ten nie jest emitowany, jeśli wystąpi wyjątek. Jest to domyślne zachowanie obiektów typu Observable: wystąpienie wyjątku oznacza również zakończenie emisji. Subskrybenci są tego świadomi;
  • wiersze 22–34: metoda [Observable.create] przyjmuje jako parametr typ [Observable.OnSubscribe]. Typ ten jest interfejsem funkcjonalnym. Pojęcie to zostało wprowadzone w Javie 8 i oznacza interfejs posiadający jedną metodę. W tym przypadku jedyną metodą interfejsu [Observable.OnSubscribe] jest następująca:
T call(Subscriber<T> subscriber)

Aby zaimplementować interfejs funkcjonalny z jedną metodą m(param1, param2, ..., paramn), można użyć następującej uproszczonej składni:

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

Tak właśnie zrobiono w wierszach 22–34:

  • [subscriber] jest parametrem metody [Observable.OnSubscribe.call];
  • wiersze 23–32: kod, który chcemy nadać metodzie [call];
  • wiersz 25: listy wyborcze są pobierane synchronicznie z warstwą [métier] wstrzykniętą w wierszu 13. Będzie więc nastąpiło oczekiwanie na wynik. Gdy wynik zostanie odebrany, jest on przekazywany do metody [onNext] subskrybenta;
  • wiersz 28: w przypadku błędu wyjątek jest przekazywany do metody [onError] subskrybenta;
  • wiersz 31: oczekuje się tylko jednego wyniku. Gdy zostanie on uzyskany (listy wyborcze lub wyjątek), subskrybent otrzymuje informację, że obserwowany proces zakończył generowanie wyników;

Należy pamiętać, że metoda [RxElectionsMetier] zwraca typ Observable<ListeElectorale[]>, a nie sam typ ListeElectorale[]. Kod wywołujący musi wywołać metodę Observable<ListeElectorale[]>.subscribe, aby kod z linii 23–33 został wykonany i zwrócił listy wyborcze za pomocą linii 25.

Kod pozostałych metod jest analogiczny:


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 {
        // wywołanie metody synchronicznej
        metier.authenticate(user);
        // zgłaszane jest zakończenie obserwowalnego
        subscriber.onCompleted();
      } catch (Exception e) {
        // przekazywanie wyjątku
        subscriber.onError(e);
      }
    });

  }

  @Override
  public Observable<ListeElectorale[]> getListesElectorales(User user) {
    return Observable.create(subscriber -> {
      try {
        // wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
        subscriber.onNext(metier.getListesElectorales(user));
        // zgłaszane jest zakończenie obserwowalnego
        subscriber.onCompleted();
      } catch (Exception e) {
        // przekazywanie wyjątku
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Integer> getNbSiegesAPourvoir(User user) {
    return Observable.create(subscriber -> {
      try {
        // wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
        subscriber.onNext(metier.getNbSiegesAPourvoir(user));
        // zgłaszane jest zakończenie obserwowalnego
        subscriber.onCompleted();
      } catch (Exception e) {
        // przekazywanie wyjątku
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Double> getSeuilElectoral(User user) {
    return Observable.create(subscriber -> {
      try {
        // wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
        subscriber.onNext(metier.getSeuilElectoral(user));
        // zgłaszane jest zakończenie obserwowalnej
        subscriber.onCompleted();
      } catch (Exception e) {
        // przekazywanie wyjątku
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // wywołanie metody synchronicznej
        metier.recordResultats(user, listesElectorales);
        // zgłaszane jest zakończenie obserwowalnego
        subscriber.onCompleted();
      } catch (Exception e) {
        // przekazywanie wyjątku
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // wywołanie metody synchronicznej, a następnie odpowiedź dla subskrybenta
        subscriber.onNext(metier.calculerSieges(user, listesElectorales));
        // zgłaszane jest zakończenie obserwowalnego
        subscriber.onCompleted();
      } catch (Exception e) {
        // przekazywanie wyjątku
        subscriber.onError(e);
      }
    });
  }
}
  • wiersze 20 i 81: metoda [onNext] subskrybenta nie jest wywoływana, ponieważ subskrybent nie oczekuje żadnych wyników;

20.1.4. Testy JUnit warstwy [métier]

  

20.1.4.1. Test01

Wracamy do testu jednostkowego [Test01] omówionego w paragrafie 17.4.4. Został on zaprojektowany do wykonywania wywołań synchronicznych do interfejsu [IElectionsMetier]. Modyfikujemy go tak, aby wykonywał synchroniczne wywołania nowego interfejsu [IRxElectionsMetier]. Możliwe jest bowiem wykonywanie synchronicznych wywołań asynchronicznego interfejsu RxJava. Kod wygląda następująco:


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 {

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

  // mapujący jSON
  private final ObjectMapper mapper = new ObjectMapper();

  // użytkownicy
  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() {
    // tworzy się tabelę 7 list kandydatów
    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);
    // oblicza się liczbę mandatów dla każdej z list
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // sprawdza się wyniki
    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() {
    // tworzy się tabelę 7 list wyborczych
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // ustala się na stałe liczbę głosów
    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);
    // oblicza się liczbę mandatów uzyskanych przez każdą z list
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // sprawdza się wyniki
    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() {
    // tworzy się tabelę zawierającą 24 listy kandydatów, z których każda ma po 1 głos
    ListeElectorale[] listes = new ListeElectorale[25];
    // wszystkie 25 list otrzyma taką samą liczbę głosów (4%)
    for (int i = 0; i < listes.length; i++) {
      listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
    }
    // obliczenie liczby mandatów – zazwyczaj powinno to dać wynik ElectionsException
    // przy progu wyborczym wynoszącym 5%
    BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
  }

  /**
   * enregistrement des résultats de l'élection
   *
   * @throws JsonProcessingException
   */
  @Test
  public void ecritureResultatsElections() throws JsonProcessingException {
    // tworzymy tabelę 7 list wyborczych
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // ustala się na stałe liczbę głosów
    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);
    // obliczamy liczbę mandatów uzyskanych przez każdą z list
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // wyświetla się wyniki
    for (int i = 0; i < listes.length; i++) {
      System.out.println(mapper.writeValueAsString(listes[i]));
    }
    // zapisuje się wyniki w bazie danych
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
    // sprawdza się wyniki
    listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // wyświetla się wyniki
    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());
  }
}

Przyjrzyjmy się zmianom:

  • wiersz 48: metoda statyczna [BlockingObservable.from(Observable).first]:
    • subskrybuje parametr obserwowalny metody [from];
    • uruchamia wykonanie kodu powiązanego z obserwowalnym;
    • czeka na otrzymanie pierwszego wyniku. Jest to zatem operacja synchroniczna;

W tym przypadku używamy metody [firstOrDefault(null)], ponieważ obserwowalna [metier.authenticate] nie zwraca wyniku po uruchomieniu. Wynikiem metody [firstOrDefault(null)] będzie zatem null, wartość, która nie jest tutaj wykorzystywana;

Ten schemat powtarzamy w pozostałej części kodu za każdym razem, gdy chcemy wywołać warstwę [métier].

Test jednostkowy [Test01] musi zakończyć się powodzeniem:

 

Zadanie do wykonania: sprawdzić, czy test [Test01] zakończy się powodzeniem.


20.1.4.2. Test02

Modyfikujemy test [Test01], aby odtąd testować interfejs asynchroniczny [IRxElectionsMetier] poprzez wykonywanie asynchronicznych wywołań jego metod.

Przyjrzyjmy się pierwszemu testowi:


  // semafor synchronizacji wątków
  private CountDownLatch latch;

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

  @Test()
  public void checkUserUser() throws InterruptedException {
    // semafor ustawiony na 1
    latch = new CountDownLatch((1));
    // operacja asynchroniczna
    electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // oczekiwanie na semafor
    latch.await();
    // weryfikacja wyników
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • wiersz 2: semafor to narzędzie służące do synchronizacji wątków między sobą. Wątki to strumienie wykonywania działające równolegle. Aby wykonać zadanie T1, wątek [Thread1] może wymagać, aby zadanie T2, wykonywane przez wątek [Thread2], zostało zakończone. Wówczas wątek ten oczekuje, aż wątek [Thread2] wyśle mu sygnał wskazujący, że zadanie T2 zostało zakończone. Istnieją różne sposoby zarządzania tą synchronizacją między dwoma wątkami. Zastosowana tutaj metoda jest następująca:
    • wiersz 10: wątek [Thread1] tworzy semafor o wartości 1;
    • wiersz 12: wątek [Thread1] tworzy i uruchamia wątek [Thread2]. Odbywa się to za pomocą następującej składni:

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

Metoda [Observable.subscribeOn] określa wątek, na którym będzie wykonywany obserwowany proces. Parametrem metody [subscribeOn] jest pula wątków. Biblioteka RxJava udostępnia kilka takich pul, dostosowanych do różnych sytuacji. Pula [Schedulers.io()] jest zalecana do operacji sieciowych;

  • (ciąg dalszy)
    • wiersze 12–13: operacja

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

wykonuje operację synchroniczną zawartą w obserwowalnym obiekcie [authenticate(user)]. Ponieważ jednak ta operacja synchroniczna jest uruchamiana w innym wątku niż wątek [Thread1], ten ostatni nie czeka na odpowiedź metody [subscribe] i przechodzi do następnej instrukcji;

  • (ciąg dalszy)
    • wiersz 23: wątek [Thread1] zatrzymuje się i czeka, aż semafor przyjmie wartość 0 (obecnie ma wartość 1);
  • wiersze 13–21: metoda [subscribe] przyjmuje jako parametry trzy funkcje lambda:
    • pierwsza, [(result)->{...}], jest wywoływana za każdym razem, gdy obserwowalna [authenticate(user)] emituje wynik [result]. W tym przypadku mamy obserwowalną [authenticate(user)], która wykonuje pewne działanie, ale nie emituje żadnego wyniku. Funkcja lambda [(result)->{}] nigdy więc nie zostanie wywołana. Dlatego jej kod jest tutaj pusty: [{}];
    • druga funkcja lambda [(th)->{...}] otrzymuje jako parametr typ [Throwable]. Jest ona wywoływana, gdy podczas wykonywania obserwowalnej funkcji wystąpi wyjątek. W tym przypadku przetwarzamy parametr [Throwable th] w następujący sposób:
      • wiersz 16: zapisujemy go w polu klasy testowej typu [ElectionsException], ponieważ wykonywana obserwowalna generuje wyłącznie ten typ wyjątku;
      • wiersz 17: ustawiamy semafor na 0, aby wskazać, że wątek [Thread2] zakończył swoją pracę;
    • trzeci [()->{...}] jest wywoływany, gdy obserwowalny nie ma już elementów do wysłania. Obsługujemy to zdarzenie w następujący sposób:
      • wiersz 20: ustawiamy semafor na 0, aby zasygnalizować, że wątek [Thread2] zakończył swoją pracę;

Należy zauważyć, że trzecia funkcja lambda nie jest wywoływana, jeśli wystąpi wyjątek. Dlatego też musieliśmy ustawić semafor na 0 również w wierszu 17;

  • wiersz 25: gdy dochodzimy do tego wiersza, obserwowalny obiekt zakończył swoją pracę. Możemy wówczas przeprowadzić te same sprawdzenia, co w teście [Test01];

Przyjrzyjmy się kolejnemu testowi:


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

  @Test
  public void calculSieges1() throws InterruptedException {
    // tworzymy tablicę zawierającą 7 list kandydackich
    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);
    // semafor ustawiony na 1
    latch = new CountDownLatch((1));
    // operacja asynchroniczna    
    // obliczamy liczbę mandatów dla każdej z list
    electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // oczekiwanie na semafor
    latch.await();
    // sprawdzanie wyników
    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());
  }
  • wiersze 20–30: asynchroniczne wykonanie obserwowalnego [electionsMetier.calculerSieges(admin, listes)];
  • wiersze 21–23: wykonanie obserwowalnego zwraca typ [ListeElectorale[]], który zapisujemy w polu klasy testowej w wierszu 3;
  • wiersze 34–48: są to weryfikacje z testu [Test01], do których dodano weryfikację z wiersza 34, sprawdzającą, czy nie wystąpił żaden wyjątek;

Cały test [Test02] jest dostępny w materiałach szkoleniowych.


Zadanie do wykonania: uruchomić test [Test02] i sprawdzić, czy zakończył się powodzeniem.


20.1.4.3. Test03

Test [Test03] działa tak samo jak test [Test01]: testuje interfejs [IRxElectionsMetier] poprzez synchroniczne wywołania tego interfejsu. Jest to kopia testu [Test02] z dwoma różnicami:

  • obserwowalne nie są już wykonywane w wątku innym niż ten, w którym wykonywane są testy. Gdy wątek [Thread1] wywołuje metodę [subscribe] obiektu obserwowalnego, metoda ta inicjuje operację HTTP skierowaną do serwera, również w wątku [Thread1]. Cała metoda [subscribe] staje się wówczas synchroniczna;
  • ponieważ pozostaje tylko jeden wątek, synchronizacja wątków staje się zbędna, a semafor znika;

Oto dwa przykłady testów:


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

  @Test()
  public void checkUserUser() throws InterruptedException {
    // operacja synchroniczna
    electionsMetier.authenticate(user)
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                    },
                    () -> {
                    });
    // weryfikacja wyników
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • wiersz 7: domyślnie metoda [electionsMetier.authenticate(user).subscribe] jest wykonywana w wątku kodu wywołującego. Mamy więc do czynienia z operacją synchroniczną;

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

  @Test
  public void calculSieges1() throws InterruptedException {
    // tworzy się tabelę 7 list kandydackich
    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);
    // operacja synchroniczna    
    // obliczanie liczby mandatów dla każdej z list
    electionsMetier.calculerSieges(admin, listes)
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                    },
                    () -> {
                    });
    // weryfikacja wyników
    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());
  }

Zadanie do wykonania: przeprowadź test [Test03] i sprawdź, czy zakończy się powodzeniem.


20.2. Krok 2

Teraz przekształcamy synchroniczną aplikację konsolową z rozdziału 17.5 w aplikację, która nadal jest synchroniczna, ale wykorzystuje asynchroniczny interfejs [RxJava, metier, DAO];

Wychodzimy od projektu [elections-console-metier-dao-security-webjson] [1] z rozdziału 17.5, który powielamy w nowym projekcie [elections-console-rxjava- metier-dao-security-webjson] [2]:

  • do [3-4]; w nowym projekcie usuwamy zależność od starej warstwy synchronicznej [métier];
  • w [5-9] dodano zależność od nowej, asynchronicznej warstwy [métier];
  • w pliku [10-14] zmieniono nazwę klasy [ElectionsConsole] na [ElectionsConsole01];

Podobnie zmienia się nazwę klasy [BootElectionsConsole] na [BootElectionsConsole01]:

 

Obecny kod klasy [BootElectionsConsole01] jest następujący:


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);
    }
}
  • wiersz 13: ponieważ zmieniono nazwę klasy [ElectionsConsole] na [ElectionsConsole01], należy teraz wpisać:

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

Wróćmy do kodu klasy [ElectionsConsole01]:


@Component
public class ElectionsConsole01 implements IElectionsUI {

    @Autowired
    private IElectionsMetier electionsMetier;

  @Autowired
  private User admin;
  
    @Override
    public void run() {
        // listy biorące udział w wyborach
        ListeElectorale[] listes;
        // wprowadzanie danych
        try (Scanner clavier = new Scanner(System.in)) {
         // pobieranie list biorących udział w wyborach z warstwy [metier]
         listes = electionsMetier.getListesElectorales(admin);
            ...
        // przeprowadza się obliczenia dotyczące mandatów
        listes=electionsMetier.calculerSieges(admin,listes);
        // zapisywanie wyników
        electionsMetier.recordResultats(admin,listes);
        ...
}

Jeśli posłużymy się przykładem testu [Test01] z paragrafu 20.1.4.1, wiersze 5, 17, 20 i 22 ulegną następującym zmianom:


@Component
public class ElectionsConsole01 implements IElectionsUI {

  @Autowired
  private IRxElectionsMetier electionsMetier;

  @Autowired
  private User admin;

  @Override
  public void run() {
    // listy startowe
    ListeElectorale[] listes;
    // wprowadzanie danych
    try (Scanner clavier = new Scanner(System.in)) {
      // pobieranie list startowych z warstwy [metier]
      listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
      ...
    // przeprowadza się obliczenia dotyczące mandatów
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    // zapisywanie wyników
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
    ...
  }

Zadanie: skonfiguruj projekt tak, aby uruchamiał klasę [BootElectionsConsole01] z trzema parametrami [SS, Heures travaillées, Jours travaillés] i sprawdź, czy wykonanie tak skonfigurowanego projektu daje oczekiwane wyniki.



Zadanie: skonfiguruj projekt tak, aby uruchamiał parę [BootElectionsConsole02, ElectionsConsole02], przy czym klasa [ElectionsConsole02] została napisana zgodnie z wzorem testu [Test02] z paragrafu 20.1.4.2.



Zadanie: skonfiguruj projekt tak, aby uruchomić parę [BootElectionsConsole03, ElectionsConsole03], przy czym klasa [ElectionsConsole03] została napisana zgodnie z wzorem testu [Test03] z paragrafu 20.1.4.3.


20.3. Krok 3

Przechodzimy teraz do przeniesienia aplikacji Swing do środowiska asynchronicznego.

Zaczynamy od skopiowania projektu [elections-swing-metier-dao-security-webjson] [1] z rozdziału 17.6 do nowego projektu [elections-swing-rxjava-metier-dao-security-webjson] [2]:

  • w [3, 4] usuwamy zależność od warstwy synchronicznej [console];
  • w warstwie [5-9] dodajemy zależność od asynchronicznej warstwy konsoli;

Warstwa [swing] będzie wykonywać rzeczywiste wywołania asynchroniczne do warstwy [métier]. Podczas wywołania jednej z jej metod będą działały dwa wątki:

  • wątek warstwy UI, który obsługuje zdarzenia;
  • wątek wejścia/wyjścia, który wykona wywołanie HTTP do serwera;

Przez cały czas trwania wywołania asynchronicznego powinniśmy wyświetlać obrazek oczekiwania oraz przycisk anulowania. Nie zrobimy tego tutaj, a zostanie to zaproponowane jako ulepszenie aplikacji. Zmiany dotyczą dwóch klas, które wywołują warstwę [métier]:

 

20.3.1. Konfiguracja Maven

W tym przypadku wykorzystamy bibliotekę [RxSwing], która rozszerza bibliotekę [RxJava] o funkcje dostępne wyłącznie w środowisku Swing. W tym celu modyfikujemy plik [pom.xml] w następujący sposób:


<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>
    <!-- warstwy dolne -->
    <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. Klasa [ElectionsConnectForm]

W trybie asynchronicznym klasa [ElectionsConnectForm] przyjmuje następujący kształt:


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;

  // odniesienie do warstwy [métier] asynchronicznej
  @Autowired
  private IRxElectionsMetier metier;

  // zalogowany użytkownik
  private User user;

  // formularz główny
  @Autowired
  private ElectionsMainForm electionsMainForm;

  // sesja UI
  @Autowired
  private UiSession uiSession;

  @Override
  protected void doConnect() {
    if (isPageValid()) {
      // uwierzytelnianie użytkownika
      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
        // brak odpowiedzi
        (result) -> {
        },
        // obsługa wyjątku
        (th) -> {
          // odnotowano błąd
          String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
          // wyświetlanie informacji
          jTextPaneErreurs.setText(info);
          jTextPaneErreurs.setCaretPosition(0);

        },
        // uwierzytelnianie zakończone
        () -> {
          // zapisano użytkownika w sesji
          uiSession.setUser(user);
          // ekran logowania jest ukryty
          setVisible(false);
          // wyświetlany jest ekran główny
          electionsMainForm.run();
        });
    }
  }

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

  @Override
  public void run() {
    // wyświetlany jest interfejs graficzny
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
  }

  private boolean isPageValid() {
    ...
  }

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

}
  • wiersze 36–63: metoda [doConnect] jest wykonywana, gdy użytkownik wybierze opcję menu [Connexion]:
 

Wszystko znajduje się w wierszu 40:


      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
  • obserwowanym procesem jest [metier.authenticate(user)];
  • zostanie on uruchomiony w wątku wejścia/wyjścia pobranym z puli [Schedulers.io()];
  • będzie obserwowany w wątku UI, który obsługuje zdarzenia interfejsu Swing [observeOn(SwingScheduler.getInstance())]. Wątek ten jest uzyskiwany za pomocą metody [SwingScheduler.getInstance()], gdzie [SwingScheduler] jest klasą dostarczaną przez bibliotekę [RxSwing]. Jest to obowiązkowe. Po uzyskaniu wyniku operacji asynchronicznej jest on często wykorzystywany do modyfikowania elementów interfejsu Swing. Interfejs ten można jednak modyfikować wyłącznie w wątku klasy UI; w przeciwnym razie wystąpi wyjątek. W związku z tym wiersze 41–61 muszą być wykonywane w wątku klasy UI. Zapewnia to tutaj metoda [observeOn(SwingScheduler.getInstance())];

Omówmy pozostałą część kodu:

  • wiersze 42–43: te wiersze służą do zachowania składni metody [subscribe]. Nigdy nie zostaną one wykonane, ponieważ proces [metier.authenticate(user)] nie zwraca żadnego wyniku;
  • wiersze 35–52: po otrzymaniu wyjątku jest on wyświetlany;
  • wiersze 54–61: wykonywane, gdy proces [metier.authenticate(user)] sygnalizuje zakończenie wysyłania danych;

20.3.3. Klasa [ElectionsMainForm]

 

20.3.3.1. Inicjalizacja interfejsu graficznego


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;

  // odwołanie do warstwy asynchronicznej [métier]
  @Autowired
  private IRxElectionsMetier metier;

  // sesja UI
  @Autowired
  private UiSession uiSession;

  // zalogowany użytkownik
  private User user;

  // szablony list JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // konkurencyjne listy
  private ListeElectorale[] listes;

  // listy wprowadzone przez użytkownika
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

  // inicjalizacje
  @Override
  protected void init() {
    // generowanie komponentów przez klasę nadrzędną
    super.init();
    // stan formularza
    Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
    Utilitaires.setEnabled(
            new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
    // wyśrodkowanie okna
    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);
    // zalogowany użytkownik
    user = uiSession.getUser();
    // inicjalizacje lokalne
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    // wystosowano żądanie list do warstwy [métier]
    metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // odpowiedź
                    listesElectorales -> {
                      // zapisywanie list
                      listes = listesElectorales;
                    },
                    // wyjątek
                    (th) -> showException(th),
                    // koniec obserwowalny
                    () -> {
                      // kolejny krok
                      doInitStep2();
                    });
  }
...
  • wiersz 46: metoda [init] jest wykonywana w momencie wyświetlenia powiązanego okna. Jej celem jest zainicjowanie poniższych komponentów [1-3]:
 
  • wiersze 71–85: asynchronicznie pobierane są listy kandydatów (komponent [1]);
  • wiersz 71: obserwowanym procesem jest [metier.getListesElectorales(user)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
  • wiersze 74–77: wynik zwrócony przez obserwowany proces jest zapisywany w polu [listes] w wierszu 38;
  • wiersz 79: ewentualny wyjątek jest obsługiwany przez następującą metodę:

  private void showException(Throwable th) {
    // wyświetla się wyjątek
    jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
    jTextPaneMessages.setCaretPosition(0);
}
  • wiersze 81–84: po zakończeniu obserwowanego procesu wykonywane są wiersze 81–84. Wiersze te nie są wykonywane, jeśli wystąpił wyjątek. Metoda [doInitStep2] realizuje etap 2 inicjalizacji w następujący sposób:

  private void doInitStep2() {
    // przypisuje się nazwy list do pola kombi jComboBoxNomsListes
    for (int i = 0; i < listes.length; i++) {
      jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
    }
    // liczba miejsc do obsadzenia
    metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // odpowiedź
                    nbSiegesAPourvoir -> {
                      // inicjuje się etykietę powiązaną z tą informacją
                      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
                    },
                    // wyjątek
                    (th) -> showException(th),
                    // koniec obserwowalny
                    () -> {
                      // kolejny etap
                      doInitStep3();
                    });
}
  • wiersze 3–5: wykorzystuje się wynik poprzedniego etapu do wypełnienia listy rozwijanej nazwami list kandydatów;
  • wiersze 7–20: asynchronicznie pobierana jest liczba miejsc do obsadzenia;
  • wiersz 7: obserwowanym procesem jest [metier.getNbSiegesAPourvoir(user)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
  • wiersze 10–13: wynik zwrócony przez proces jest wykorzystywany do aktualizacji interfejsu graficznego;
  • wiersz 15: wyświetlany jest ewentualny wyjątek;
  • wiersze 17–20: po otrzymaniu sygnału zakończenia obserwowalnego przechodzi się do etapu 3 procesu inicjalizacji;

Etap 3 inicjalizacji jest realizowany przez następujący kod:


  private void doInitStep3() {
    // próg wyborczy
    metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // odpowiedź
                    seuilElectoral -> {
                      // inicjujemy etykietę powiązaną z tą informacją
                      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
                    },
                    // wyjątek
                    (th) -> showException(th),
                    // koniec obserwowalny
                    () -> {
                    });
}
  • wiersze 3–4: asynchroniczne żądanie progu wyborczego;
  • wiersz 3: obserwowanym procesem jest [metier.getSeuilElectoral(user)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
  • wiersze 6–9: wynik zwrócony przez proces służy do aktualizacji interfejsu graficznego;
  • wiersz 11: wyświetlany jest ewentualny wyjątek;
  • wiersze 13–14: po otrzymaniu sygnału zakończenia obserwowalnego nie podejmuje się żadnych działań: proces inicjalizacji interfejsu graficznego został zakończony;

20.3.3.2. Obliczanie liczby mandatów uzyskanych przez poszczególne listy

Metoda [doCalculer] służy do obliczania liczby mandatów zdobytych przez poszczególne listy:


  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // obliczanie liczby mandatów
    String info = null;
    metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // przetwarzanie wyniku
                    result -> consumeResultSieges(result),
                    // przetwarzanie wyjątku
                    th -> showException(th),
                    // koniec obserwowalny
                    () -> {
                    }
            );
}
  • wiersze 6–15: asynchronicznie obliczana jest liczba mandatów zdobytych przez poszczególne listy;
  • wiersz 6: obserwowanym procesem jest [metier.calculerSieges(user, tListesSaisies)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
  • wiersz 9: wynik zwrócony przez proces jest wykorzystywany przez metodę [consumeResultSieges];
  • wiersz 11: wyświetlany jest ewentualny wyjątek;
  • wiersze 13–14: po otrzymaniu sygnału zakończenia obserwowanego obiektu nie podejmuje się żadnych działań;

W wierszu 9 metoda [consumeResultSieges] wykorzystuje wynik zwrócony przez obserwowany proces, aktualizując listy kandydatów wraz z ich polami [sieges, elimine]:


  private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
    // zapisywanie wyniku
    this.tListesSaisies = tListesSaisies;
    // wyświetlanie wyników
    modèleRésultats.clear();
    for (int i = 0; i < tListesSaisies.length; i++) {
      modèleRésultats.addElement(tListesSaisies[i].toString());
    }
    // aktualizacja stanu formularza
    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é");
}
  • wiersze 4–14: uzyskany wynik służy do aktualizacji interfejsu graficznego;

20.3.3.3. Rejestracja wyników wyborów

Rejestracja wyników wyborów odbywa się za pomocą następującej metody [doEnregistrer]:


  @Override
  protected void doEnregistrer() {
    // wysyłanie żądania zapisu do warstwy [métier]
    metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // przetwarzanie wyniku – tutaj go nie ma
                    (param) -> {
                    },
                    // obsługa wyjątku
                    (th) -> showException(th),
                    // koniec obserwowalny
                    () -> {
                      // aktualizacja formularza
                      Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
                      Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
                      jTextPaneMessages.setText("Enregistrement des résultats réalisé");
                    }
            );
}
  • wiersze 4–17: wyniki wyborów są zapisywane asynchronicznie;
  • wiersz 4: obserwowanym procesem jest [metier.recordResultats(user, tListesSaisies)]. Jest on wykonywany w wątku wejścia/wyjścia [subscribeOn(Schedulers.io())] i obserwowany w wątku UI oraz [observeOn(SwingScheduler.getInstance()];
  • wiersze 7–8: te wiersze nigdy nie zostaną wykonane, ponieważ obserwowany proces nie zwraca wyniku;
  • wiersz 10: wyświetlany jest ewentualny wyjątek;
  • wiersze 14–16: po otrzymaniu sygnału zakończenia obserwowalnego obiektu aktualizowany jest interfejs graficzny;

Zadanie do wykonania: sprawdź, czy aplikacja Swing działa. Następnie zmodyfikuj interfejs graficzny i kod tak, aby podczas operacji asynchronicznej z serwerem WWW / jSON pojawiał się obrazek oczekiwania oraz opcja anulowania bieżącej operacji.