Skip to content

17. [TD]: zabezpieczenie serwera internetowego / jSON wyborów

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, usługa internetowa / jSON zabezpieczona, klient / serwer

Teraz zastosujemy to, czego nauczyliśmy się w poprzednim rozdziale, do projektu TD dotyczącego wyborów. Architektura będzie wyglądać następująco:

Proces będzie przebiegał w następujący sposób:

  • pisanie kodu serwera;
  • pisanie kodu klienta bez warstwy [ui], ale z testem JUnit warstwy [métier];
  • zapis po stronie klienta z warstwą [ui];

17.1. Support

 

Projekty z tego rozdziału znajdują się w folderze [support / chap-17]. Skrypt SQL służy do wygenerowania bazy danych niezbędnej do przeprowadzenia testów.

17.2. Baza danych

Baza danych serwera bezpiecznego powinna teraz zawierać tabele [USERS], [ROLES] oraz [USERS_ROLES]:

 

Skrypt SQL służący do generowania bazy danych jest dostępny w dokumentacji pomocy technicznej.

17.3. Serwer zabezpieczony

Aby skonfigurować bezpieczny serwer wyborczy, wystarczy skopiować i wkleić przykładowy projekt [intro-spring-security-server-01]:


Czynności do wykonania: należy zmienić nazwy pakietów zgodnie z instrukcją zawartą w pliku [1].


Po wykonaniu tej czynności 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-security-webjson-metier-dao-spring-data</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>elections-security-webjson-metier-dao-spring-data</name>
    <description>elections spring security</description>

    <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>
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-webjson-metier-dao-spring-data</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security -->
        ...
    </dependencies>
    <!-- wtyczki -->
    <build>
        ...
    </build>

</project>
  • w wierszach 23–27 należy zastąpić zależność od projektu [intro-server-webjson-01] zależnością od projektu [elections-webjson-metier-dao-spring-data] omówionego w punkcie 12;
  • w wierszach 4–7 wprowadzić cechy nowego projektu;

Pozostaje nam jeszcze zmodyfikować klasy konfiguracyjne projektu:

[DaoConfig]


package elections.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(basePackages = { "elections.security.repositories" })
@ComponentScan(basePackages = { "elections.security.dao" })
@Import({ elections.dao.config.DaoConfig.class })
public class DaoConfig {

    // stałe
    final static private String[] ENTITIES_PACKAGES = { "elections.dao.entities", "elections.security.entities" };


    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}

Zmiany dotyczą następujących wierszy:

  • wiersze 8, 9, 14: należy wpisać prawidłowe nazwy pakietów;
  • wiersz 10: importowana klasa to teraz [elections.dao.config.DaoConfig] z projektu [elections-webjson-metier-dao-spring-data];

Uwaga: jak już wspomniano, ta konfiguracja działa tylko wtedy, gdy klasa [elections.dao.config.DaoConfig] posiada adnotację [@Configuration]. Należy to sprawdzić.

[SecurityConfig]


package elections.security.config;

...

@EnableWebSecurity
@ComponentScan(basePackages = { "elections.security.service" })
@Import({ WebConfig.class, DaoConfig.class })
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...
}

Zmiany dotyczą następujących wierszy:

  • wiersz 6: zmień nazwę pakietu;

To wszystko. Przeprowadź wstępny test, uruchamiając test JUnit []:

  

Uruchom klasę startową [Boot], a następnie, korzystając z rozszerzenia Chrome [Advanced Rest Client], przeprowadź następujący test:

W pliku [1] znajduje się żądanie. W pliku [3] znajduje się odpowiedź. W pliku [2] nagłówek HTTP stanowi nagłówek uwierzytelniający użytkownika [admin, admin]: Authorization:Basic YWRtaW46YWRtaW4=

Zapytaj teraz o listy biorące udział w konkursie:

17.4. Klient serwera bezpiecznego bez warstwy [ui]

Skopiuj i wklej projekt [elections-ui-metier-dao-webjson] do projektu [elections-metier-dao-security-webjson].


Zadanie do wykonania: w projekcie [1] zmień nazwy pakietów w razie potrzeby i usuń te z warstwy [ui] oraz z klas startowych [boot].


Teraz postąpimy tak, jak opisano w punkcie 16.5.

17.4.1. Konfiguracja Maven

Zmienia się jedynie część identyfikująca projekt:


<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-webjson</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <description>Client jUnit du serveur web / jSON</description>
    <name>elections-metier-dao-security-webjson</name>
...

17.4.2. Refaktoryzacja warstwy [métier]

  

Interfejs [IElectionsMetier] zmienia się w następujący sposób:


package elections.security.client.metier;

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

public interface IElectionsMetier {

    // uwierzytelnianie
    public void authenticate(User user);

    // pobieranie list kandydatów
    public ListeElectorale[] getListesElectorales(User user);

    // liczba mandatów do obsadzenia
    public int getNbSiegesAPourvoir(User user);

    // próg wyborczy
    public double getSeuilElectoral(User user);

    // rejestracja wyników
    public void recordResultats(User user, ListeElectorale[] listesElectorales);

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

}

Pierwszym parametrem wszystkich metod jest użytkownik, który chce korzystać z zasobów bezpiecznego serwera.

Implementacja [ElectionsMetier] zmienia się w następujący sposób:


package elections.security.client.metier;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.security.client.dao.IClientDao;
import elections.security.client.entities.ElectionsConfig;
import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;

@Component
public class ElectionsMetier implements IElectionsMetier {

  @Autowired
  private IClientDao dao;
  @Autowired
  private ApplicationContext context;

  // konfiguracja wyborów
  private ElectionsConfig electionsConfig;

  private ElectionsConfig getElectionsConfig(User user) {
    if(electionsConfig!=null){
      return electionsConfig;
    }
    // mapery jSON
    ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
    try {
      // zapytanie
      Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse(user, "/getElectionsConfig", null),
              new TypeReference<Response<ElectionsConfig>>() {
      });
      // błąd?
      if (response.getStatus() != 0) {
        // rzucono 1 wyjątek
        throw new ElectionsException(response.getStatus(), response.getMessages());
      } else {
        electionsConfig = response.getBody();
        return electionsConfig;
      }
    } catch (ElectionsException e1) {
      throw e1;
    } catch (IOException | RuntimeException e2) {
      throw new ElectionsException(100, e2);
    }
  }

  @Override
  public ListeElectorale[] getListesElectorales(User user) {
   ...
  }

  @Override
  public int getNbSiegesAPourvoir(User user) {
    return getElectionsConfig(user).getNbSiegesAPourvoir();
  }

  @Override
  public double getSeuilElectoral(User user) {
    return getElectionsConfig(user).getSeuilElectoral();
  }

  @Override
  public void recordResultats(User user, ListeElectorale[] listesElectorales) {
    ...
  }

  @Override
  public ListeElectorale[] calculerSieges(User user, ListeElectorale[] listesElectorales) {
    ...
  }

  @Override
  public void authenticate(User user) {
    dao.getResponse(user, "/authenticate", null);
  }
}

Zadanie: uzupełnij powyższy kod.


17.4.3. Konfiguracja Spring

  

Klasa [MetierConfig] zmienia się w następujący sposób:


package elections.security.client.config;

...

@ComponentScan({ "elections.security.client.dao", "elections.security.client.metier" })
public class MetierConfig {

W wierszu 5 należy zaktualizować pakiety do przeszukiwania.

17.4.4. Test JUnit warstwy [métier]

  

Klasa testowa [Test01] zmienia się w następujący sposób:


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.IElectionsMetier;

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

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

    // mapper 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 {
            electionsMetier.authenticate(user);
        } catch (ElectionsException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getErreurs().get(0));
    }

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

    @Test()
    public void checkUserAdmin() {
        ElectionsException se = null;
        try {
            electionsMetier.authenticate(admin);
        } 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 = electionsMetier.calculerSieges(admin,listes);
        // 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ę zawierającą 7 list wyborczych
        ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
        // 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 = electionsMetier.calculerSieges(admin,listes);
        // 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 1 głos
        ListeElectorale[] listes = new ListeElectorale[25];
        // wszystkie 25 list będzie miało 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);
        }
        // obliczanie liczby mandatów – zazwyczaj powinno to dać wynik ElectionsException
        // przy progu wyborczym wynoszącym 5%
        electionsMetier.calculerSieges(admin,listes);
    }

    /**
     * enregistrement des résultats de l'élection
     * 
     * @throws JsonProcessingException
     */
    @Test
    public void ecritureResultatsElections() throws JsonProcessingException {
        // tworzy się tabelę 7 list wyborczych
        ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
        // 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 = electionsMetier.calculerSieges(admin,listes);
        // wyświetla się wyniki
        for (int i = 0; i < listes.length; i++) {
            System.out.println(mapper.writeValueAsString(listes[i]));
        }
        // zapisywanie wyników w bazie danych
        electionsMetier.recordResultats(admin,listes);
        // sprawdza się wyniki
        listes = electionsMetier.getListesElectorales(admin);
        // 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());
    }
}

Zadanie: zapoznaj się z tym testem i sprawdź, czy się powiodło.


17.5. Klient serwera zabezpieczonego warstwą [console]

W programie NetBeans otwieramy projekty Maven [elections-metier-dao-security-webjson] i [elections-ui-metier-dao-webjson], a następnie tworzymy nowy projekt [elections-console-metier-dao-security-webjson] poprzez skopiowanie i wklejenie projektu [elections-ui-metier-dao-webjson]:

Większość klas nowego projektu [3] pochodzi z projektu [elections-ui-metier-dao-webjson] oraz [2], który był klientem niezabezpieczonego serwera WWW / jSON:

Aby utworzyć nowy projekt Maven, należy postępować w następujący sposób:

  • skopiuj i wklej projekt [elections-ui-metier-dao-webjson] do projektu [elections-ui-metier-dao-security-webjson];
  • w nowym projekcie:
    • usuń pakiety [elections.client.dao, elections.client.metier, elections.client.entities];
    • w pakiecie [elections.client.ui] zachowaj tylko klasę konsoli [ElectionsConsole] i jej interfejs [IElectionsUI];
    • zmień nazwy pakietów zgodnie z instrukcją na [3];
    • napraw problemy z importowaniem w różnych klasach za pomocą pliku [clic droit / Fix Imports];

Na tym etapie nowy projekt zawiera wiele błędów.

17.5.1. Konfiguracja Maven

Plik [pom.xml] nowego projektu wygląda następująco:


<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-console-metier-dao-security-webjson</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elections-console-metier-dao-security-webjson</name>
    <description>couche console du client web / jSON</description>

    <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>
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-metier-dao-security-webjson</artifactId>
            <version>0.0.1-SNAPSHOT</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 4–8: identyfikator Maven nowego projektu;
  • wiersze 22–26: zależność od projektu [elections-metier-dao-security-webjson], który właśnie utworzyliśmy w punkcie 17.4 i który dostarcza warstwy [DAO] oraz [métier];

17.5.2. Konfiguracja Spring

  

Klasa [UiConfig] wygląda następująco:


package elections.security.client.config;

import elections.security.client.entities.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

@Import(MetierConfig.class)
@ComponentScan(basePackages = {"elections.security.client.console","elections.security.client.swing"})
public class UiConfig {

  // administrator
  private final User ADMIN = new User("admin", "admin");

  @Bean
  private User admin() {
    return ADMIN;
  }

}
  • wiersz 8: importujemy klasę [elections.security.client.config.MetierConfig] z projektu [elections-metier-dao-security-webjson] omówionego w paragrafie 17.4;
  • wiersz 9: deklaruje się pakiety, w których znajdują się komponenty Spring;
  • wiersze 12–18: bean [admin] to użytkownik [admin, admin], który będzie używany przez aplikację konsolową. Przypomnijmy, że jest to jedyny użytkownik uprawniony do wysyłania zapytań do zabezpieczonego serwera WWW / jSON;

17.5.3. Aplikacja startowa konsoli

  

Klasa [ElectionsConsole] zmienia się w następujący sposób:


package elections.security.client.console;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IElectionsMetier;

@Component
public class ElectionsConsole implements IElectionsUI {

    @Autowired
    private IElectionsMetier electionsMetier;

  @Autowired
  private User admin;
  
    @Override
    public void run() {
        // listy biorące udział w konkursie
        ListeElectorale[] listes;
        // wprowadzanie danych
        try (Scanner clavier = new Scanner(System.in)) {
            // pobieranie list startowych z warstwy [metier]
            listes = electionsMetier.getListesElectorales(admin);
            // przeprowadza się liczenie głosów
            System.out.println("Il y a " + listes.length
                    + " listes en compétition. Veuillez indiquer le nombre de voix de chacune d'elles :");
            for (int i = 0; i < listes.length; i++) {
                boolean saisieOK = false;
                while (!saisieOK) {
                    System.out.print("Nombre de voix de la liste [" + listes[i].getNom() + "] : ");
                    try {
                        listes[i].setVoix(Integer.parseInt(clavier.nextLine()));
                        saisieOK = true;
                    } catch (ElectionsException | NumberFormatException ex) {
                        System.out.println("Nombre de voix incorrect. Veuillez recommencer");
                    }
                }
            }
        }
        // przeprowadza się obliczenia dotyczące mandatów
        listes=electionsMetier.calculerSieges(admin,listes);
        // rejestruje się wyniki
        electionsMetier.recordResultats(admin,listes);
        // sortowanie list w porządku malejącym według liczby głosów
        Arrays.sort(listes, new CompareListesElectorales());
        // wyświetla się je
        System.out.println("\nRésultats de l'élection\n");
        for (int i = 0; i < listes.length; i++) {
            System.out.println(listes[i]);
        }
    }

    // klasa porównywania list wyborczych
    class CompareListesElectorales implements Comparator<ListeElectorale> {

        // porównanie dwóch list kandydatów pod względem liczby głosów
        @Override
        public int compare(ListeElectorale listeElectorale1, ListeElectorale listeElectorale2) {
            // porównuje się liczbę głosów tych dwóch list
            int nbVoix1 = listeElectorale1.getVoix();
            int nbVoix2 = listeElectorale2.getVoix();
            if (nbVoix1 < nbVoix2) {
                return +1;
            } else {
                if (nbVoix1 > nbVoix2)
                    return -1;
                else
                    return 0;
            }
        }
    }

}

Klasa ElectionsConsole zmienia się w bardzo niewielkim stopniu. Należy jedynie pamiętać, że obecnie metody warstwy [métier] wymagają jako pierwszego parametru użytkownika (wiersze 31, 49, 51). Jest on dostarczany poprzez wstrzyknięcie z wierszy 21–22. Wstrzyknięty użytkownik to ten, który ma uprawnienia do wysyłania zapytań do serwera WWW / jSON.


Zadanie do wykonania: przetestuj aplikację konsolową (pamiętaj, aby wcześniej uruchomić serwer zabezpieczony).


17.6. Klient serwera zabezpieczonego z warstwą [swing]

Tworzymy nowy projekt NetBeans o nazwie [elections-swing-metier-dao-security-webjson] poprzez skopiowanie i wklejenie projektu [elections-ui-metier-dao-webjson]:

W nowym projekcie [2]:

  • usuń pakiety [elections.client.dao, elections.client.metier, elections.client.entities];
  • w pakiecie [elections.client.ui] usuń klasy [ElectionsConsole, IElectionsUI];
  • zmień nazwy pakietów zgodnie z instrukcją w [2];
  • w pakiecie [elections.security.client.swing] zmień nazwę klasy [AbstractElectionsSwing], która implementuje interfejs graficzny, na [AbstractElectionsMainForm], a klasy [ElectionsSwing], która implementuje procedury obsługi zdarzeńinterfejsu graficznego na [ElectionsMainForm];
  • należy rozwiązać problemy związane z importowaniem w różnych klasach za pomocą pliku [clic droit / Fix Imports];

Na tym etapie występuje wiele błędów.

17.6.1. Konfiguracja Maven

Plik [pom.xml] zmienia się 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-metier-dao-security-webjson</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elections-swing-metier-dao-security-webjson</name>
    <description>couche swing du client web / jSON</description>

    <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>
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-console-metier-dao-security-webjson</artifactId>
            <version>0.0.1-SNAPSHOT</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 4–8: identyfikator Maven nowego projektu;
  • wiersze 22–26: zależność od projektu konsolowego, który właśnie omówiliśmy w paragrafie 17.5;

17.6.2. Konfiguracja Spring

Projekt ten wykorzystuje konfigurację Spring z importowanego projektu konsolowego (patrz punkt 17.5.2).

17.6.3. Widoki aplikacji Swing

 

W tym projekcie zostaną wykorzystane dwa widoki:

  • widok logowania [1] jest nowy. Służy on do uwierzytelniania użytkownika, który chce korzystać z aplikacji. Wykorzystuje on klasy:
    • [AbstractElectionsConnectForm], która implementuje widok;
    • [ElectionsConnectForm], która obsługuje zdarzenia widoku;
  • widok [2] jest znany. Jest to widok używany do tej pory. Wykorzystuje on następujące klasy:
    • [AbstractElectionsMainForm], która implementuje widok;
    • [ElectionsMainForm], która obsługuje zdarzenia widoku;

17.6.4. Sesja

  

Gdy aplikacja Swing posiada wiele widoków, należy znaleźć sposób, aby jeden widok mógł przekazywać informacje do innego widoku. Wykorzystujemy koncepcję stosowaną w tworzeniu stron internetowych: sesję, która pozwala widokom powiązanym z konkretnym użytkownikiem na wymianę informacji. Koncepcja ta zostanie tutaj zaimplementowana za pomocą singletonu Spring, który zostanie wstrzyknięty do dwóch klas zarządzających zdarzeniami obu widoków:


package elections.security.client.swing;

import elections.security.client.entities.User;
import org.springframework.stereotype.Component;

@Component
public class UiSession {
  
    // zalogowany użytkownik
  private User user;
  
  // metody pobierające i ustawiające
... 
}
  • wiersz 6: klasa [UiSession] jest komponentem Springa;
  • wiersz 10: służy ona wyłącznie do jednego celu: zapamiętania użytkownika logującego się za pomocą widoku [1]. Widok [2], który musi znać ten identyfikator, pobierze go właśnie stamtąd;

17.6.5. Klasa Boot

  

Klasa startowa aplikacji graficznej wygląda następująco:


package elections.security.client.boot;

import elections.security.client.console.IElectionsUI;

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

    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsConnectForm", IElectionsUI.class);
    }
}
  • wiersz 5: klasa [BootElectionsSwing] rozszerza klasę [AbstractBootElections] zdefiniowaną w projekcie konsolowym znajdującym się w zależnościach projektu;
  • wiersze 10–13: klasa [BootElectionsSwing] spowoduje wyświetlenie widoku logowania [ElectionsConnectForm];
  • wiersze 6–8: zostanie wykonana metoda [run] tej klasy;

17.6.6. Klasa [ElectionsMainForm]

Klasa [ElectionsMainForm] to ta, która wcześniej nosiła nazwę [ElectionsSwing]. Obsługuje ona zdarzenia widoku [AbstractElectionsMainForm]. Jej kod zmienia się w następujący sposób:


package elections.security.client.swing;

import elections.security.client.console.IElectionsUI;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;

import javax.swing.DefaultListModel;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IElectionsMetier;

@Component
public class ElectionsMainForm extends AbstractElectionsMainForm implements IElectionsUI {

  private static final long serialVersionUID = 1L;

  // odwołanie do warstwy [métier]
  @Autowired
  private IElectionsMetier 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;

  // listy biorące udział w konkursie
  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();
    // inicjalizacje lokalne
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    String info;
    boolean erreur = false;
    try {
      // pobieranie list z warstwy [métier]
      listes = metier.getListesElectorales(user);
      // powiązanie nazw list z polem kombi jComboBoxNomsListes
      for (int i = 0; i < listes.length; i++) {
        jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
      }
      // oraz parametry wyborów
      int nbSiegesAPourvoir = metier.getNbSiegesAPourvoir(user);
      double seuilElectoral = metier.getSeuilElectoral(user);
      // inicjuje się etykiety powiązane z tymi dwiema informacjami
      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
      // komunikat o pomyślnym zakończeniu
      info = "Source de données lue avec succès";
    } catch (ElectionsException ex1) {
      // odnotowuje się błąd
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
      erreur = true;
    } catch (RuntimeException ex2) {
      // odnotowuje się błąd
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
      erreur = true;
    }
    // błąd?
    if (erreur) {
      // wyświetla się informacja
      jTextPaneMessages.setText(info);
      jTextPaneMessages.setCaretPosition(0);
      return;
    } else {
      jTextPaneMessages.setText("");
    }
    // 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);

  }
...

  @Override
  public void run() {
    // zapisanie danych użytkownika
    user = uiSession.getUser();
    // inicjalizacja okna
    init();
    setVisible(true);
  }
}
  • wiersze 32–33: wstrzyknięcie sesji aplikacji;
  • wiersze 112–119: widok zostanie aktywowany, tak jak poprzednio, za pomocą metody [run];
  • wiersz 115: pobieramy użytkownika z sesji. Został on tam umieszczony przez kod widoku logowania [ElectionsConnectForm]. Następnie kod jest modyfikowany tak, aby pierwszym parametrem wywołań warstwy [métier] był ten użytkownik (na przykład wiersze 63, 69–70);

17.6.7. Widok [AbstractElectionsConnectForm]

  

Zbuduj w NetBeans następujący formularz:

 

Klasa ta nie będzie sama implementować metody obsługującej kliknięcie opcji menu [Connexion]. Będzie ona wywoływać metodę [doConnecter] zadeklarowaną jako abstrakcyjną, a sama klasa widoku zostanie zadeklarowana jako abstrakcyjna. Usunięta zostanie statyczna metoda [main], która została wygenerowana automatycznie.

17.6.8. Obsługa zdarzeń w widoku logowania

  

Klasa [ElectionsConnectForm] implementuje obsługę zdarzeń widoku logowania w następujący sposób:


package elections.security.client.swing;

import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.User;
import elections.security.client.metier.IElectionsMetier;
import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.SwingUtilities;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ElectionsConnectForm extends AbstractElectionsConnectForm implements IElectionsUI {

    private static final long serialVersionUID = 1L;

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

  // zalogowany użytkownik
  private User user;

  // formularz główny
  @Autowired
  private ElectionsMainForm electionsMainForm;
  
  // sesja UI
  @Autowired
  private UiSession uiSession;
  
  @Override
  protected void doConnect() {
    String info = null;
    try {
      if (isPageValid()) {
        // uwierzytelnianie użytkownika
        metier.authenticate(user);
        // użytkownik jest zapisywany w sesji
        uiSession.setUser(user);
        // ekran logowania jest ukryty
        setVisible(false);
        // wyświetlany jest ekran główny
        electionsMainForm.run();
      }
    } catch (ElectionsException ex1) {
      // rejestruje się błąd
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
    } catch (RuntimeException ex2) {
      // odnotowuje się błąd
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
    }
    // błąd?
    if (info != null) {
      // wyświetlana jest informacja
      jTextPaneErreurs.setText(info);
      jTextPaneErreurs.setCaretPosition(0);
    }
  }

  // inicjalizacje
  @Override
  protected void init() {
    // generowanie komponentów przez klasę nadrzędną
    super.init();
    // inicjalizacje lokalne
...
    // wyśrodkowanie okna
...
  }

  @Override
  public void run() {
    // wyświetlanie interfejsu graficznego
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
  }

  private boolean isPageValid() {
    ...
  }

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

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

}
  • wiersze 73–82: metoda [run], która zostanie wywołana przez klasę startową [BootElectionsSwing];
  • wiersze 26–27: wstrzyknięcie odwołania do widoku nr 2, który ma zostać wyświetlony, jeśli użytkownik zostanie rozpoznany;
  • wiersze 30–31: wstawienie sesji aplikacji;
  • wiersz 84: metoda [isPageValid] wykonuje dwie czynności:
    • sprawdza, czy identyfikator nie jest pusty (hasło może być puste);
    • tworzy instancję pola User w wierszu 23 na podstawie wprowadzonych danych;

Zadanie: uzupełnij kod klasy.



Zadanie: Przetestuj aplikację.