Skip to content

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

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, Webdienst / jSON gesichert, Client / Server

Wir wenden nun das im vorherigen Kapitel Gelernte auf das Wahlsystem TD an. Die Architektur sieht wie folgt aus:

Der Ablauf wird wie folgt sein:

  • Erstellung des Servers;
  • Programmierung des Clients ohne die [ui]-Schicht, jedoch mit einem Test der JUnit-Schicht für die [métier]-Schicht;
  • Schreiben vom Client mit der Schicht [ui];

17.1. Support

 

Die Projekte dieses Kapitels befinden sich im Ordner [support / chap-17]. Das Skript SQL dient dazu, die für die Tests erforderliche Datenbank zu erstellen.

17.2. Die Datenbank

Die Datenbank des sicheren Servers muss nun die Tabellen [USERS], [ROLES] und [USERS_ROLES] enthalten:

 

Das Skript SQL zur Erstellung der Datenbank ist im Dokumenten-Support verfügbar.

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:


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


Anschließend ä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 vom Projekt [elections-webjson-metier-dao-spring-data], das in Abschnitt 12 behandelt wurde;
  • Geben Sie in den Zeilen 4–7 die Merkmale des neuen Projekts ein;

Nun müssen wir noch die Konfigurationsklassen des Projekts ändern:

[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 {

    // Konstanten
    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 richtigen 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] hat. Bitte überprüfen Sie diesen Punkt.

[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 Namen des Pakets;

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

  

Führen Sie die Boot-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 Header HTTP der Authentifizierungsheader des Benutzers [admin, admin]: Authorization:Basic YWRtaW46YWRtaW4=

Fragen Sie jetzt nach den konkurrierenden Listen:

17.4. Der Client des sicheren Servers ohne die Schicht [ui]

Führen Sie ein Kopieren/Einfügen des Projekts [elections-ui-metier-dao-webjson] in das Projekt [elections-metier-dao-security-webjson] durch.


Zu erledigende Aufgabe: Benennen Sie in [1] die Pakete bei Bedarf um und löschen Sie diejenigen aus der Ebene [ui] sowie aus den Startklassen [boot].


Wir gehen nun genauso vor wie in Abschnitt 16.5 beschrieben.

17.4.1. Maven-Konfiguration

Nur der Teil 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 Schicht [métier]

  

Die Schnittstelle [IElectionsMetier] entwickelt sich wie folgt weiter:


package elections.security.client.metier;

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

public interface IElectionsMetier {

    // Authentifizierung
    public void authenticate(User user);

    // Wettbewerbslisten abrufen
    public ListeElectorale[] getListesElectorales(User user);

    // Anzahl der zu vergebenden Sitze
    public int getNbSiegesAPourvoir(User user);

    // Wahlhürde
    public double getSeuilElectoral(User user);

    // Erfassung der Ergebnisse
    public void recordResultats(User user, ListeElectorale[] listesElectorales);

    // Berechnung der Sitze
    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 [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;

  // Konfiguration der Wahl
  private ElectionsConfig electionsConfig;

  private ElectionsConfig getElectionsConfig(User user) {
    if(electionsConfig!=null){
      return electionsConfig;
    }
    // Zuordnungsfunktionen jSON
    ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
    try {
      // Abfrage
      Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse(user, "/getElectionsConfig", null),
              new TypeReference<Response<ElectionsConfig>>() {
      });
      // Fehler?
      if (response.getStatus() != 0) {
        // Es wird 1 Ausnahme ausgelöst
        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 durchsuchenden Pakete müssen aktualisiert werden.

17.4.4. Der Test JUnit der Schicht [métier]

  

Die Testklasse [Test01] entwickelt 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 {

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

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

    // Benutzer
    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() {
        // Es wird die Tabelle mit den 7 Kandidatenlisten erstellt
        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);
        // die Sitze jeder einzelnen Liste werden berechnet
        listes = electionsMetier.calculerSieges(admin,listes);
        // die Ergebnisse werden überprüft
        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() {
        // Es wird eine Tabelle mit den 7 Kandidatenlisten erstellt
        ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
        // Die Stimmen werden fest vorgegeben
        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);
        // Die von jeder Liste erzielten Sitze werden berechnet
        listes = electionsMetier.calculerSieges(admin,listes);
        // Die Ergebnisse werden überprüft
        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() {
        // Es wird eine Tabelle mit 24 Kandidatenlisten erstellt, wobei jede Liste 1 Stimme erhält
        ListeElectorale[] listes = new ListeElectorale[25];
        // Alle 25 Listen erhalten die gleiche Anzahl an Stimmen (4 %)
        for (int i = 0; i < listes.length; i++) {
            listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
        }
        // Sitzberechnung – normalerweise müsste es ElectionsException lauten
        // bei einer Wahlhürde von 5 %
        electionsMetier.calculerSieges(admin,listes);
    }

    /**
     * enregistrement des résultats de l'élection
     * 
     * @throws JsonProcessingException
     */
    @Test
    public void ecritureResultatsElections() throws JsonProcessingException {
        // Es wird eine Tabelle mit den 7 Kandidatenlisten erstellt
        ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
        // Die Stimmen werden fest vorgegeben
        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);
        // die von jeder Liste erzielten Sitze werden berechnet
        listes = electionsMetier.calculerSieges(admin,listes);
        // Die Ergebnisse werden angezeigt
        for (int i = 0; i < listes.length; i++) {
            System.out.println(mapper.writeValueAsString(listes[i]));
        }
        // Die Ergebnisse werden in der Datenbank gespeichert
        electionsMetier.recordResultats(admin,listes);
        // Die Ergebnisse werden überprüft
        listes = electionsMetier.getListesElectorales(admin);
        // Die Ergebnisse werden angezeigt
        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: Machen Sie sich mit diesem Test vertraut und stellen Sie sicher, dass er erfolgreich durchläuft.


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

Mit NetBeans öffnen wir die Maven-Projekte [elections-metier-dao-security-webjson] und [elections-ui-metier-dao-webjson] und erstellen dann ein neues Projekt [elections-console-metier-dao-security-webjson] durch Kopieren und Einfügen des Projekts [elections-ui-metier-dao-webjson]:

Die meisten Klassen des neuen Projekts [3] stammen aus den Projekten [elections-ui-metier-dao-webjson] und [2], die als Clients des ungesicherten Webservers jSON fungierten:

Gehen Sie wie folgt vor, 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 angegeben in [3] um;
    • Beheben Sie die Importprobleme in den verschiedenen Klassen mit [clic droit / Fix Imports];

Zu diesem Zeitpunkt weist das neue Projekt zahlreiche Fehler auf.

17.5.1. Maven-Konfiguration

Die Datei [pom.xml] des neuen Projekts lautet 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-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 Schichten [DAO] und [métier] 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 {

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

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

}
  • Zeile 8: Die Klasse [elections.security.client.config.MetierConfig] aus dem in Abschnitt 17.4 behandelten Projekt [elections-metier-dao-security-webjson] wird importiert;
  • Zeile 9: Hier werden die Pakete deklariert, in denen sich die Spring-Beans befinden;
  • Zeilen 12–18: Die Bean [admin] ist der Benutzer [admin, admin], der von der Konsolenanwendung verwendet wird. Zur Erinnerung: Dies ist der einzige Benutzer, der berechtigt ist, den gesicherten Webserver /jSON abzufragen;

17.5.3. Die Boot-Anwendung der Konsole

  

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() {
        // die Teilnehmerlisten
        ListeElectorale[] listes;
        // Dateneingabe
        try (Scanner clavier = new Scanner(System.in)) {
            // Anforderung der Wahllisten von der Ebene [metier]
            listes = electionsMetier.getListesElectorales(admin);
            // Die Stimmen werden erfasst
            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");
                    }
                }
            }
        }
        // Die Sitze werden berechnet
        listes=electionsMetier.calculerSieges(admin,listes);
        // Die Ergebnisse werden gespeichert
        electionsMetier.recordResultats(admin,listes);
        // Sortierung der Listen in absteigender Reihenfolge der Stimmen
        Arrays.sort(listes, new CompareListesElectorales());
        // Anzeige der Listen
        System.out.println("\nRésultats de l'élection\n");
        for (int i = 0; i < listes.length; i++) {
            System.out.println(listes[i]);
        }
    }

    // Vergleichsklasse für Wahllisten
    class CompareListesElectorales implements Comparator<ListeElectorale> {

        // Vergleich zweier Kandidatenlisten nach der Anzahl der Stimmen
        @Override
        public int compare(ListeElectorale listeElectorale1, ListeElectorale listeElectorale2) {
            // Die Stimmen dieser beiden Listen werden verglichen
            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. Man muss lediglich beachten, dass die Methoden der Schicht [métier] nun als ersten Parameter einen Benutzer verlangen (Zeilen 31, 49, 51). Dieser wird durch die Injektion in den Zeilen 21–22 bereitgestellt. Der injizierte Benutzer ist derjenige, der berechtigt ist, den Webserver /jSON abzufragen.


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


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

Wir erstellen ein neues NetBeans-Projekt [elections-swing-metier-dao-security-webjson] durch Kopieren und Einfügen des Projekts [elections-ui-metier-dao-webjson]:

Im neuen Projekt [2]:

  • Löschen Sie die Pakete [elections.client.dao, elections.client.metier, elections.client.entities];
  • im Paket [elections.client.ui] die Klassen [ElectionsConsole, IElectionsUI] löschen;
  • benennen Sie die Pakete wie in [2] angegeben um;
  • Im Paket [elections.security.client.swing] benennen Sie die Klasse [AbstractElectionsSwing], die die grafische Benutzeroberfläche implementiert, in [AbstractElectionsMainForm] um, und die Klasse [ElectionsSwing], die die Ereignisbehandler dergrafischen Benutzeroberflächen in „[ElectionsMainForm]“ um;
  • beheben Sie die Importprobleme in den verschiedenen Klassen mithilfe von [clic droit / Fix Imports];

Derzeit gibt es zahlreiche Fehler.

17.6.1. Maven-Konfiguration

Die Datei [pom.xml] entwickelt 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-Identität des neuen Projekts;
  • Zeilen 22–26: eine Abhängigkeit vom Konsolenprojekt, das wir gerade in Abschnitt 17.5 behandelt 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

 

In diesem Projekt werden zwei Ansichten verwendet:

  • Die Anmeldesicht [1] ist neu. Sie dient dazu, den Benutzer zu authentifizieren, der die Anwendung nutzen möchte. Sie verwendet die folgenden Klassen:
    • [AbstractElectionsConnectForm], die die Ansicht implementiert;
    • [ElectionsConnectForm], die die Ereignisse der Ansicht verwaltet;
  • Die Ansicht [2] ist bekannt. Sie wurde bisher verwendet. Sie nutzt die folgenden Klassen:
    • [AbstractElectionsMainForm], die die Ansicht implementiert;
    • [ElectionsMainForm], das die Ereignisse der Ansicht verwaltet;

17.6.4. Die Sitzung

  

Wenn eine Swing-Anwendung mehrere Ansichten hat, muss ein Weg gefunden werden, damit eine Ansicht Informationen an eine andere Ansicht weitergeben kann. Wir nutzen ein aus der Webentwicklung bekanntes Konzept: die Sitzung, die es den mit einem bestimmten Benutzer verbundenen Ansichten ermöglicht, Informationen auszutauschen. Dieses Konzept wird hier mithilfe eines Spring-Singletons implementiert, das in die beiden Klassen injiziert wird, die die Ereignisse der beiden Ansichten verwalten:


package elections.security.client.swing;

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

@Component
public class UiSession {
  
    // der angemeldete Benutzer
  private User user;
  
  // Getter und Setter
... 
}
  • Zeile 6: Die Klasse [UiSession] ist eine Spring-Komponente;
  • Zeile 10: Sie dient nur einem Zweck: dem Speichern des Benutzers, der sich über die Ansicht [1] anmeldet. Die Ansicht [2], die diesen Benutzernamen benötigt, ruft ihn dort ab;

17.6.5. Die Boot-Klasse

  

Die Boot-Klasse der grafischen 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 zu den Projektabhängigkeiten gehört;
  • 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] hieß zuvor [ElectionsSwing]. Sie verwaltet die Ereignisse der 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;

  // Referenz zur Schicht [métier]
  @Autowired
  private IElectionsMetier metier;

  // Sitzung UI
  @Autowired
  private UiSession uiSession;

  // angemeldeter Benutzer
  private User user;

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

  // die Listen im Wettbewerb
  private ListeElectorale[] listes;

  // Vom Benutzer eingegebene Listen
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

  // Initialisierungen
  @Override
  protected void init() {
    // Generierung der Komponenten durch die übergeordnete Klasse
    super.init();
    // lokale Initialisierungen
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    String info;
    boolean erreur = false;
    try {
      // Die Listen werden von der Schicht [métier] angefordert
      listes = metier.getListesElectorales(user);
      // Die Namen der Listen werden dem Kombinationsfeld zugeordnet jComboBoxNomsListes
      for (int i = 0; i < listes.length; i++) {
        jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
      }
      // sowie die Wahlparameter
      int nbSiegesAPourvoir = metier.getNbSiegesAPourvoir(user);
      double seuilElectoral = metier.getSeuilElectoral(user);
      // werden die mit diesen beiden Informationen verbundenen Beschriftungen initialisiert
      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
      // Erfolgsmeldung
      info = "Source de données lue avec succès";
    } catch (ElectionsException ex1) {
      // der Fehler wird protokolliert
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
      erreur = true;
    } catch (RuntimeException ex2) {
      // Der Fehler wird protokolliert
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
      erreur = true;
    }
    // Fehler?
    if (erreur) {
      // Information wird angezeigt
      jTextPaneMessages.setText(info);
      jTextPaneMessages.setCaretPosition(0);
      return;
    } else {
      jTextPaneMessages.setText("");
    }
    // Formularstatus
    Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
    Utilitaires.setEnabled(
            new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
    // Fenster zentrieren
    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() {
    // Benutzerdaten speichern
    user = uiSession.getUser();
    // Fenster initialisieren
    init();
    setVisible(true);
  }
}
  • Zeilen 32–33: Einbindung der Anwendungssitzung;
  • Zeilen 112–119: Die Ansicht wird wie zuvor über ihre Methode [run] aktiviert;
  • Zeile 115: Der Benutzer wird aus der Sitzung abgerufen. Dieser wurde zuvor durch den Code der Anmeldesicht [ElectionsConnectForm] dort hinterlegt. Anschließend wird der Code so geändert, dass der erste Parameter der Aufrufe der Schicht [métier] dieser Benutzer ist (beispielsweise Zeilen 63, 69–70);

17.6.7. Die Ansicht [AbstractElectionsConnectForm]

  

Erstellen Sie mit NetBeans das folgende Formular:

 

Die Klasse wird die Methode zur Verarbeitung des Klicks auf die Menüoption [Connexion] nicht selbst implementieren. Sie wird eine als abstrakt deklarierte Methode [doConnecter] aufrufen, und die Ansichtsklasse wird ihrerseits als abstrakt deklariert. Die automatisch generierte statische Methode [main] wird entfernt.

17.6.8. Die Ereignisbehandlung der Anmeldesicht

  

Die Klasse [ElectionsConnectForm] implementiert die Ereignisbehandlung der Anmeldesicht 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;

    // Referenz auf die Ebene [métier]
  @Autowired
  private IElectionsMetier metier;

  // Benutzer angemeldet
  private User user;

  // Hauptformular
  @Autowired
  private ElectionsMainForm electionsMainForm;
  
  // Sitzung UI
  @Autowired
  private UiSession uiSession;
  
  @Override
  protected void doConnect() {
    String info = null;
    try {
      if (isPageValid()) {
        // Benutzerauthentifizierung
        metier.authenticate(user);
        // Der Benutzer wird in der Sitzung gespeichert
        uiSession.setUser(user);
        // Die Anmeldeseite wird ausgeblendet
        setVisible(false);
        // Die Hauptansicht wird angezeigt
        electionsMainForm.run();
      }
    } catch (ElectionsException ex1) {
      // Der Fehler wird protokolliert
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
    } catch (RuntimeException ex2) {
      // Der Fehler wird protokolliert
      info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
    }
    // Fehler?
    if (info != null) {
      // Die Information wird angezeigt
      jTextPaneErreurs.setText(info);
      jTextPaneErreurs.setCaretPosition(0);
    }
  }

  // Initialisierungen
  @Override
  protected void init() {
    // Generierung der Komponenten durch die übergeordnete Klasse
    super.init();
    // Lokale Initialisierungen
...
    // Fenster zentrieren
...
  }

  @Override
  public void run() {
    // Die grafische Benutzeroberfläche wird angezeigt
    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 die 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 überprüft, ob die ID nicht leer ist (das Passwort darf hingegen leer sein);
    • sie initialisiert das Feld „User“ in Zeile 23 mit den Eingaben;

Aufgabe: Vervollständigen Sie den Code der Klasse.



Aufgabe: Testen Sie die Anwendung.