Skip to content

17. [TD]: Absicherung des Webservers / JSON für Wahlen

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, sicherer Webdienst / JSON, Client / Server

Wir werden nun das im vorigen Kapitel Gelernte auf die Wahlaufgabe anwenden. Die Architektur sieht wie folgt aus:

Der Prozess läuft wie folgt ab:

  • Schreiben des Servers;
  • Entwicklung des Clients ohne die [ui]-Schicht, aber mit einem JUnit-Test für die [business]-Schicht;
  • Entwicklung des Clients mit der [UI]-Schicht;

17.1. Support

 

Die Projekte für dieses Kapitel befinden sich im Ordner [support / chap-17]. Das SQL-Skript dient zur Erstellung der für die Tests erforderlichen Datenbank.

17.2. Die Datenbank

Die Datenbank auf dem Secure Server muss nun die Tabellen [USERS], [ROLES] und [USERS_ROLES] enthalten:

 

Das SQL-Skript zum Erstellen der Datenbank finden Sie im Support-Bereich des Dokuments.

17.3. Der sichere Server

Um den sicheren Wahlserver einzurichten, kopieren Sie einfach das Beispielprojekt [intro-spring-security-server-01] und fügen Sie es ein:


Aufgabe: Benennen Sie die Pakete wie in [1] gezeigt um.


Sobald dies erledigt ist, ändern wir die Datei [pom.xml] wie folgt:


<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>
    <!-- plugins -->
    <build>
        ...
    </build>

</project>
  • Ersetzen Sie in den Zeilen 23–27 die Abhängigkeit vom Projekt [intro-server-webjson-01] durch die Abhängigkeit vom Projekt [elections-webjson-metier-dao-spring-data], das in Absatz 12 beschrieben wurde;
  • Geben Sie in den Zeilen 4–7 die Eigenschaften des neuen Projekts ein;

Wir müssen noch die Konfigurationsklassen des Projekts anpassen:

[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 {
 
    // constants
    final static private String[] ENTITIES_PACKAGES = { "elections.dao.entities", "elections.security.entities" };
 
 
    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }
 
}

Die Änderungen befinden sich in den folgenden Zeilen:

  • Zeilen 8, 9, 14: Geben Sie die korrekten Paketnamen ein;
  • Zeile 10: Die importierte Klasse ist nun [elections.dao.config.DaoConfig] aus dem Projekt [elections-webjson-metier-dao-spring-data];

Hinweis: Wie bereits erwähnt, funktioniert diese Konfiguration nur, wenn die Klasse [elections.dao.config.DaoConfig] die Annotation [@Configuration] enthält. Bitte überprüfen Sie dies.

[SecurityConfig]


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

Die Änderungen befinden sich in den folgenden Zeilen:

  • Zeile 6: Ändern Sie den Paketnamen;

Das war's. Führen Sie einen ersten Test durch, indem Sie den JUnit-Test [] ausführen:

  

Führen Sie die Klasse [Boot] aus und führen Sie anschließend mit der Chrome-Erweiterung [Advanced Rest Client] den folgenden Test durch:

In [1] die Anfrage. In [3] die Antwort. In [2] ist der HTTP-Header der Header zur Benutzerauthentifizierung [admin, admin]: Authorization:Basic YWRtaW46YWRtaW4=

Fordern Sie nun die konkurrierenden Listen an:

17.4. Der Secure-Server-Client ohne die [ui]-Schicht

Kopieren Sie das Projekt [elections-ui-metier-dao-webjson] und fügen Sie es in das Projekt [elections-metier-dao-security-webjson] ein.


Aufgabe: Benennen Sie in [1] die Pakete bei Bedarf um und entfernen Sie diejenigen aus der [ui]-Ebene sowie die [boot]-Startklassen.


Wir fahren nun fort, wie in Abschnitt 16.5 beschrieben.

17.4.1. Maven-Konfiguration

Nur der Abschnitt zur Projektidentifikation ändert sich:


<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. Refactoring der [Business]-Schicht

  

Die Schnittstelle [IElectionsMetier] entwickelt sich wie folgt:


package elections.security.client.metier;
 
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
 
public interface IElectionsMetier {
 
    // authentication
    public void authenticate(User user);
 
    // get the lists in competition
    public ListeElectorale[] getListesElectorales(User user);
 
    // the number of seats to be filled
    public int getNbSiegesAPourvoir(User user);
 
    // the electoral threshold
    public double getSeuilElectoral(User user);
 
    // recording results
    public void recordResultats(User user, ListeElectorale[] listesElectorales);
 
    // calculating seats
    public ListeElectorale[] calculerSieges(User user, ListeElectorale[] listesElectorales);
 
}

Alle Methoden haben als ersten Parameter den Benutzer, der die Ressourcen des sicheren Servers nutzen möchte.

Die Implementierung von [ElectionsMetier] entwickelt sich wie folgt:


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;
 
  // election configuration
  private ElectionsConfig electionsConfig;
 
  private ElectionsConfig getElectionsConfig(User user) {
    if(electionsConfig!=null){
      return electionsConfig;
    }
    // mappers jSON
    ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
    try {
      // request
      Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse(user, "/getElectionsConfig", null),
              new TypeReference<Response<ElectionsConfig>>() {
      });
      // mistake?
      if (response.getStatus() != 0) {
        // 1 exception is thrown
        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);
  }
}

Aufgabe: Vervollständigen Sie den obigen Code.


17.4.3. Spring-Konfiguration

  

Die Klasse [MetierConfig] entwickelt sich wie folgt:


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

Zeile 5: Die zu scannenden Pakete müssen aktualisiert werden.

17.4.4. Der JUnit-Test für die [Business]-Schicht

  

Die Testklasse [Test01] ändert sich wie folgt:


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 {
 
    // layer [electionsMetier]
    @Autowired
    private IElectionsMetier electionsMetier;
 
    // mapper jSON
    private final ObjectMapper mapper = new ObjectMapper();
 
    // users
    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() {
        // create the table of 7 candidate lists
        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);
        // the seats for each list are calculated
        listes = electionsMetier.calculerSieges(admin,listes);
        // check results
        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() {
        // create the table of 7 candidate lists
        ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
        // the voices are hard-fixed
        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);
        // the seats obtained by each list are calculated
        listes = electionsMetier.calculerSieges(admin,listes);
        // check results
        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() {
        // we create a table of 24 candidate lists, each with 1 vote
        ListeElectorale[] listes = new ListeElectorale[25];
        // all 25 lists will have the same number of votes (4%)
        for (int i = 0; i < listes.length; i++) {
            listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
        }
        // calculation of seats - normally there should be a ElectionsException
        // with an electoral threshold of 5%
        electionsMetier.calculerSieges(admin,listes);
    }
 
    /**
     * enregistrement des résultats de l'élection
     * 
     * @throws JsonProcessingException
     */
    @Test
    public void ecritureResultatsElections() throws JsonProcessingException {
        // create the table of 7 candidate lists
        ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
        // the voices are hard-fixed
        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);
        // the seats obtained by each list are calculated
        listes = electionsMetier.calculerSieges(admin,listes);
        // display results
        for (int i = 0; i < listes.length; i++) {
            System.out.println(mapper.writeValueAsString(listes[i]));
        }
        // results are entered into the database
        electionsMetier.recordResultats(admin,listes);
        // check results
        listes = electionsMetier.getListesElectorales(admin);
        // display results
        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());
    }
}

Aufgabe: Verstehe diesen Test und überprüfe, ob er erfolgreich ist.


17.5. Der Client des sicheren Servers mit einer [console]-Schicht

In NetBeans öffnen wir die Maven-Projekte [elections-business-dao-security-webjson] und [elections-ui-business-dao-webjson] und erstellen dann ein neues Projekt [elections-console-business-dao-security-webjson], indem wir das Projekt [elections-ui-business-dao-webjson] kopieren und einfügen:

Die meisten Klassen im neuen Projekt [3] stammen aus dem Projekt [elections-ui-metier-dao-webjson] [2], das als Client für den ungesicherten Web-/JSON-Server diente:

Befolgen Sie diese Schritte, um das neue Maven-Projekt zu erstellen:

  • Kopieren Sie das Projekt [elections-ui-metier-dao-webjson] und fügen Sie es in das Projekt [elections-ui-metier-dao-security-webjson] ein;
  • Im neuen Projekt:
    • Löschen Sie die Pakete [elections.client.dao, elections.client.metier, elections.client.entities];
    • Behalten Sie im Paket [elections.client.ui] nur die Konsolenklasse [ElectionsConsole] und deren Schnittstelle [IElectionsUI] bei;
    • Benennen Sie die Pakete wie in [3] angegeben um;
    • Beheben Sie Importprobleme in den verschiedenen Klassen durch [Rechtsklick / Importe korrigieren];

Zu diesem Zeitpunkt enthält das neue Projekt zahlreiche Fehler.

17.5.1. Maven-Konfiguration

Die [pom.xml]-Datei für das neue Projekt sieht wie folgt aus:


<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>
  • Zeilen 4–8: die Maven-ID des neuen Projekts;
  • Zeilen 22–26: die Abhängigkeit vom Projekt [elections-metier-dao-security-webjson], das wir gerade in Abschnitt 17.4 erstellt haben und das die [DAO]- und [business]-Schichten bereitstellt;

17.5.2. Spring-Konfiguration

  

Die Klasse [UiConfig] sieht wie folgt aus:


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 {
 
  // director
  private final User ADMIN = new User("admin", "admin");
 
  @Bean
  private User admin() {
    return ADMIN;
  }
 
}
  • Zeile 8: Wir importieren die Klasse [elections.security.client.config.MetierConfig] aus dem in Abschnitt 17.4 behandelten Projekt [elections-metier-dao-security-webjson];
  • Zeile 9: Wir deklarieren die Pakete, in denen sich die Spring-Beans befinden;
  • Zeilen 12–18: Die [admin]-Bean ist der Benutzer [admin, admin], der von der Konsolenanwendung verwendet wird. Zur Erinnerung: Dies ist der einzige Benutzer, der berechtigt ist, den sicheren Web-/JSON-Server abzufragen;

17.5.3. Die Konsolen-Boot-Anwendung

  

Die Klasse [ElectionsConsole] entwickelt sich wie folgt:


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() {
        // competing lists
        ListeElectorale[] listes;
        // data entry
        try (Scanner clavier = new Scanner(System.in)) {
            // lists in competition are requested from the [metier] layer
            listes = electionsMetier.getListesElectorales(admin);
            // we enter the votes
            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");
                    }
                }
            }
        }
        // we calculate the number of seats
        listes=electionsMetier.calculerSieges(admin,listes);
        // we record the results
        electionsMetier.recordResultats(admin,listes);
        // lists sorted in descending order of votes
        Arrays.sort(listes, new CompareListesElectorales());
        // we display them
        System.out.println("\nRésultats de l'élection\n");
        for (int i = 0; i < listes.length; i++) {
            System.out.println(listes[i]);
        }
    }
 
    // electoral list comparison class
    class CompareListesElectorales implements Comparator<ListeElectorale> {
 
        // comparison of two candidate lists by number of votes
        @Override
        public int compare(ListeElectorale listeElectorale1, ListeElectorale listeElectorale2) {
            // we compare the votes of these two lists
            int nbVoix1 = listeElectorale1.getVoix();
            int nbVoix2 = listeElectorale2.getVoix();
            if (nbVoix1 < nbVoix2) {
                return +1;
            } else {
                if (nbVoix1 > nbVoix2)
                    return -1;
                else
                    return 0;
            }
        }
    }
 
}

Die Klasse „ElectionsConsole“ ändert sich kaum. Beachten Sie lediglich, dass die Methoden in der [Business]-Schicht nun einen Benutzer als ersten Parameter benötigen (Zeilen 31, 49, 51). Dieser wird durch die Injektion in den Zeilen 21–22 bereitgestellt. Der injizierte Benutzer ist derjenige, der zur Abfrage des Webservers / jSON berechtigt ist.


Aufgabe: Testen Sie die Konsolenanwendung (denken Sie daran, zuerst den sicheren Server zu starten).


17.6. Der Client des sicheren Servers mit einer [swing]-Schicht

Wir erstellen ein neues NetBeans-Projekt [elections-swing-business-dao-security-webjson], indem wir das Projekt [elections-ui-business-dao-webjson] kopieren und einfügen:

Im neuen Projekt [2]:

  • Löschen Sie die Pakete [elections.client.dao, elections.client.metier, elections.client.entities];
  • Löschen Sie im Paket [elections.client.ui] die Klassen [ElectionsConsole, IElectionsUI];
  • benennen Sie die Pakete wie in [2] angegeben um;
  • Benennen Sie im Paket [elections.security.client.swing] die Klasse [AbstractElectionsSwing], die die grafische Benutzeroberfläche implementiert, in [AbstractElectionsMainForm] um, und die Klasse [ElectionsSwing], die die Ereignisbehandler der grafischen Benutzeroberfläche implementiert, in [ElectionsMainForm];
  • Beheben Sie die Importprobleme in den verschiedenen Klassen, indem Sie mit der rechten Maustaste klicken und [Importe korrigieren] auswählen;

Zu diesem Zeitpunkt gibt es zahlreiche Fehler.

17.6.1. Maven-Konfiguration

Die Datei [pom.xml] ändert sich wie folgt:


<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>
  • Zeilen 4–8: die Maven-ID des neuen Projekts;
  • Zeilen 22–26: eine Abhängigkeit vom Konsolenprojekt, das wir gerade in Abschnitt 17.5 betrachtet haben;

17.6.2. Spring-Konfiguration

Dieses Projekt verwendet die Spring-Konfiguration des Konsolenprojekts, das es importiert (siehe Abschnitt 17.5.2).

17.6.3. Die Ansichten der Swing-Anwendung

 

Dieses Projekt verwendet zwei Ansichten:

  • Die [1] Anmeldesicht ist neu. Sie dient zur Authentifizierung des Benutzers, der die Anwendung nutzen möchte. Sie verwendet die folgenden Klassen:
    • [AbstractElectionsConnectForm], die die Ansicht implementiert;
    • [ElectionsConnectForm], die die Ereignisse der Ansicht verarbeitet;
  • Die Ansicht [2] ist bekannt. Sie wurde bisher verwendet. Sie nutzt die folgenden Klassen:
    • [AbstractElectionsMainForm], die die Ansicht implementiert;
    • [ElectionsMainForm], die die Ereignisse der Ansicht verarbeitet;

17.6.4. Die Sitzung

  

Wenn eine Swing-Anwendung mehrere Ansichten enthält, ist ein Mechanismus erforderlich, der es einer Ansicht ermöglicht, Informationen an eine andere Ansicht weiterzugeben. Wir nutzen ein Konzept aus der Webentwicklung: die Sitzung, die es Ansichten, die einem bestimmten Benutzer zugeordnet sind, ermöglicht, Informationen auszutauschen. Dieses Konzept wird hier mithilfe eines Spring-Singletons implementiert, das in die beiden Klassen injiziert wird, die Ereignisse für beide Ansichten verarbeiten:


package elections.security.client.swing;
 
import elections.security.client.entities.User;
import org.springframework.stereotype.Component;
 
@Component
public class UiSession {
 
    // the connected user
  private User user;
 
  // getters and setters
... 
}
  • Zeile 6: Die Klasse [UiSession] ist eine Spring-Komponente;
  • Zeile 10: Sie dient nur einem Zweck: dem Speichern des Benutzers, der sich über Ansicht [1] anmeldet. Ansicht [2], die diesen Benutzer kennen muss, ruft ihn von dort ab;

17.6.5. Die Boot-Klasse

  

Die Startklasse für die grafische Anwendung lautet wie folgt:


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);
    }
}
  • Zeile 5: Die Klasse [BootElectionsSwing] erweitert die Klasse [AbstractBootElections], die im Konsolenprojekt definiert ist, das in den Projektabhängigkeiten enthalten ist;
  • Zeilen 10–13: Die Klasse [BootElectionsSwing] zeigt die Anmeldemaske [ElectionsConnectForm] an;
  • Zeilen 6–8: Die Methode [run] dieser Klasse wird ausgeführt;

17.6.6. Die Klasse [ElectionsMainForm]

Die Klasse [ElectionsMainForm] ist diejenige, die zuvor [ElectionsSwing] hieß. Sie verarbeitet Ereignisse für die Ansicht [AbstractElectionsMainForm]. Ihr Code entwickelt sich wie folgt:


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;
 
  // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
 
  // session UI
  @Autowired
  private UiSession uiSession;
 
  // logged-in user
  private User user;
 
  // list templates JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // competing lists
  private ListeElectorale[] listes;
 
  // user-entered lists
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
 
  // initializations
  @Override
  protected void init() {
    // generation of components by the parent class
    super.init();
    // local initializations
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    String info;
    boolean erreur = false;
    try {
      // lists are requested from the [business] layer
      listes = metier.getListesElectorales(user);
      // associate list names with the jComboBoxNomsListes combo
      for (int i = 0; i < listes.length; i++) {
        jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
      }
      // and election parameters
      int nbSiegesAPourvoir = metier.getNbSiegesAPourvoir(user);
      double seuilElectoral = metier.getSeuilElectoral(user);
      // we initialize the labels linked to these two pieces of information
      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
      // message of success
      info = "Source de données lue avec succès";
    } catch (ElectionsException ex1) {
      // we note the error
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
      erreur = true;
    } catch (RuntimeException ex2) {
      // we note the error
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
      erreur = true;
    }
    // mistake?
    if (erreur) {
      // display info
      jTextPaneMessages.setText(info);
      jTextPaneMessages.setCaretPosition(0);
      return;
    } else {
      jTextPaneMessages.setText("");
    }
    // form status
    Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
    Utilitaires.setEnabled(
            new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
    // center window
    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() {
    // user memory
    user = uiSession.getUser();
    // window initialization
    init();
    setVisible(true);
  }
}
  • Zeilen 32–33: Einfügen der Anwendungssitzung;
  • Zeilen 112–119: Die Ansicht wird wie zuvor über ihre [run]-Methode aktiviert;
  • Zeile 115: Der Benutzer wird aus der Sitzung abgerufen. Dieser Benutzer wurde vom Code in der Anmeldesicht [ElectionsConnectForm] dort hinterlegt. Der Code wird anschließend so angepasst, dass der erste Parameter bei Aufrufen der [business]-Schicht dieser Benutzer ist (z. B. Zeilen 63, 69–70);

17.6.7. Die Ansicht [AbstractElectionsConnectForm]

  

Erstellen Sie mit NetBeans das folgende Formular:

 

Die Klasse implementiert die Methode, die den Klick auf die Menüoption [Login] verarbeitet, nicht selbst. Sie ruft eine als abstrakt deklarierte Methode [doConnecter] auf, und die Ansichtsklasse wird selbst als abstrakt deklariert. Wir entfernen die automatisch generierte statische Methode [main].

17.6.8. Ereignisbehandlung für die Anmeldeansicht

  

Die Klasse [ElectionsConnectForm] implementiert die Ereignisbehandlung für die Anmeldeseite wie folgt:


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;
 
    // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
 
  // logged-in user
  private User user;
 
  // main form
  @Autowired
  private ElectionsMainForm electionsMainForm;
 
  // session UI
  @Autowired
  private UiSession uiSession;
 
  @Override
  protected void doConnect() {
    String info = null;
    try {
      if (isPageValid()) {
        // user authentication
        metier.authenticate(user);
        // the user is saved in the session
        uiSession.setUser(user);
        // connection view is hidden
        setVisible(false);
        // the main view is displayed
        electionsMainForm.run();
      }
    } catch (ElectionsException ex1) {
      // we note the error
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
    } catch (RuntimeException ex2) {
      // we note the error
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
    }
    // mistake?
    if (info != null) {
      // display info
      jTextPaneErreurs.setText(info);
      jTextPaneErreurs.setCaretPosition(0);
    }
  }
 
  // initializations
  @Override
  protected void init() {
    // generation of components by the parent class
    super.init();
    // local initializations
...
    // center window
...
  }
 
  @Override
  public void run() {
    // the graphical interface is displayed
    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) {
...
  }
 
}
  • Zeilen 73–82: die Methode [run], die von der Boot-Klasse [BootElectionsSwing] aufgerufen wird;
  • Zeilen 26–27: Einfügen einer Referenz auf Ansicht Nr. 2, die angezeigt werden soll, wenn der Benutzer erkannt wird;
  • Zeilen 30–31: Einfügen der Anwendungssitzung;
  • Zeile 84: Die Methode [isPageValid] führt zwei Schritte aus:
    • Sie prüft, ob der Benutzername nicht leer ist (das Passwort darf leer sein);
    • instanziiert das Feld „User“ aus Zeile 23 mit der Eingabe;

Aufgabe: Vervollständige den Klassencode.



Aufgabe: Teste die Anwendung.