Skip to content

17. [TD]: Proteção do servidor web / JSON para eleições

Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências, serviço web seguro / JSON, cliente / servidor

Vamos agora aplicar o que aprendemos no capítulo anterior à tarefa das eleições. A arquitetura será a seguinte:

O processo decorrerá da seguinte forma:

  • Escrever o servidor;
  • Escrever o cliente sem a camada [ui], mas com um teste JUnit para a camada [business];
  • escrever o cliente com a camada [UI];

17.1. Suporte

 

Os projetos para este capítulo podem ser encontrados na pasta [support / chap-17]. O script SQL é utilizado para gerar a base de dados necessária para os testes.

17.2. A base de dados

A base de dados do servidor seguro deve agora conter as tabelas [USERS], [ROLES] e [USERS_ROLES]:

 

O script SQL para gerar a base de dados está disponível na secção de suporte do documento.

17.3. O servidor seguro

Para configurar o servidor de eleições seguro, basta copiar e colar o projeto de exemplo [intro-spring-security-server-01]:


Tarefa: Renomeie os pacotes conforme mostrado em [1].


Depois de fazer isso, modificamos o ficheiro [pom.xml] da seguinte forma:


<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>
  • Nas linhas 23–27, substitua a dependência do projeto [intro-server-webjson-01] pela dependência do projeto [elections-webjson-metier-dao-spring-data] discutido no parágrafo 12;
  • Nas linhas 4–7, introduza as propriedades do novo projeto;

Ainda precisamos de modificar as classes de configuração do projeto:

[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;
    }
 
}

As alterações encontram-se nas seguintes linhas:

  • linhas 8, 9, 14: introduza os nomes de pacotes corretos;
  • linha 10: a classe importada é agora [elections.dao.config.DaoConfig] do projeto [elections-webjson-metier-dao-spring-data];

Nota: Conforme mencionado, esta configuração só funciona se a classe [elections.dao.config.DaoConfig] tiver a anotação [@Configuration]. Por favor, verifique isto.

[SecurityConfig]


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

As alterações encontram-se nas seguintes linhas:

  • linha 6: altere o nome do pacote;

É tudo. Faça um teste inicial executando o teste JUnit []:

  

Execute a classe [Boot] e, em seguida, utilize a extensão do Chrome [Advanced Rest Client] para realizar o seguinte teste:

Em [1], o pedido. Em [3], a resposta. Em [2], o cabeçalho HTTP é o cabeçalho de autenticação do utilizador [admin, admin]: Authorization:Basic YWRtaW46YWRtaW4=

Agora solicite as listas concorrentes:

17.4. O cliente do servidor seguro sem a camada [ui]

Copie e cole o projeto [elections-ui-metier-dao-webjson] no projeto [elections-metier-dao-security-webjson].


Tarefa: Em [1], renomeie os pacotes, se necessário, e remova os da camada [ui] e as classes de inicialização [boot].


Vamos agora prosseguir conforme descrito na secção 16.5.

17.4.1. Configuração do Maven

Apenas a secção de identificação do projeto é alterada:


<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. Reestruturação da camada [de negócios]

  

A interface [IElectionsMetier] evolui da seguinte forma:


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);
 
}

Todos os métodos têm como primeiro parâmetro o utilizador que pretende utilizar os recursos do servidor seguro.

A implementação do [ElectionsMetier] evolui da seguinte forma:


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);
  }
}

Tarefa: Complete o código acima.


17.4.3. Configuração do Spring

  

A classe [MetierConfig] evolui da seguinte forma:


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

Linha 5: Os pacotes a serem verificados devem ser atualizados.

17.4.4. O teste JUnit para a camada [de negócios]

  

A classe de teste [Test01] é alterada da seguinte forma:


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());
    }
}

Tarefa: Compreenda este teste e verifique se ele é aprovado.


17.5. O cliente do servidor seguro com uma camada [console]

No NetBeans, abrimos os projetos Maven [elections-business-dao-security-webjson] e [elections-ui-business-dao-webjson] e, em seguida, criamos um novo projeto [elections-console-business-dao-security-webjson] copiando e colando o projeto [elections-ui-business-dao-webjson]:

A maioria das classes no novo projeto [3] provém do projeto [elections-ui-metier-dao-webjson] [2], que era o cliente do servidor web/JSON não seguro:

Siga estes passos para compilar o novo projeto Maven:

  • Copie e cole o projeto [elections-ui-metier-dao-webjson] no projeto [elections-ui-metier-dao-security-webjson];
  • No novo projeto:
    • elimine os pacotes [elections.client.dao, elections.client.metier, elections.client.entities];
    • no pacote [elections.client.ui], mantenha apenas a classe de consola [ElectionsConsole] e a sua interface [IElectionsUI];
    • renomeie os pacotes conforme indicado em [3];
    • corrija os problemas de importação nas várias classes clicando com o botão direito do rato / Fix Imports;

Nesta fase, o novo projeto contém inúmeros erros.

17.5.1. Configuração do Maven

O ficheiro [pom.xml] para o novo projeto é o seguinte:


<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>
  • linhas 4–8: o ID Maven do novo projeto;
  • linhas 22–26: a dependência do projeto [elections-metier-dao-security-webjson] que acabámos de criar na Secção 17.4, que fornece as camadas [DAO] e [business];

17.5.2. Configuração do Spring

  

A classe [UiConfig] é a seguinte:


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;
  }
 
}
  • Linha 8: Importamos a classe [elections.security.client.config.MetierConfig] do projeto [elections-metier-dao-security-webjson] discutido na Secção 17.4;
  • linha 9: declaramos os pacotes onde se encontram os beans Spring;
  • linhas 12–18: o bean [admin] é o utilizador [admin, admin] que será utilizado pela aplicação de consola. Recorde-se que este é o único utilizador autorizado a consultar o servidor web/JSON seguro;

17.5.3. A aplicação de arranque da consola

  

A classe [ElectionsConsole] evolui da seguinte forma:


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;
            }
        }
    }
 
}

A classe ElectionsConsole sofre poucas alterações. Basta lembrar que agora os métodos na camada [business] requerem um utilizador como primeiro parâmetro (linhas 31, 49, 51). Este é fornecido pela injeção nas linhas 21–22. O utilizador injetado é aquele autorizado a consultar o servidor web / jSON.


Tarefa: Teste a aplicação de consola (lembre-se de iniciar primeiro o servidor seguro).


17.6. O cliente do servidor seguro com uma camada [swing]

Criamos um novo projeto NetBeans [elections-swing-business-dao-security-webjson] copiando e colando o projeto [elections-ui-business-dao-webjson]:

No novo projeto [2]:

  • elimine os pacotes [elections.client.dao, elections.client.metier, elections.client.entities];
  • no pacote [elections.client.ui], elimine as classes [ElectionsConsole, IElectionsUI];
  • renomeie os pacotes conforme indicado em [2];
  • no pacote [elections.security.client.swing], renomeie a classe [AbstractElectionsSwing], que implementa a interface gráfica do utilizador, para [AbstractElectionsMainForm], e a classe [ElectionsSwing], que implementa os manipuladores de eventos da interface gráfica do utilizador, para [ElectionsMainForm];
  • corrija os problemas de importação nas várias classes clicando com o botão direito do rato e selecionando [Corrigir Importações];

Nesta fase, existem inúmeros erros.

17.6.1. Configuração do Maven

O ficheiro [pom.xml] é alterado da seguinte forma:


<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>
  • linhas 4–8: o ID Maven do novo projeto;
  • linhas 22–26: uma dependência do projeto de consola que acabámos de examinar na Secção 17.5;

17.6.2. Configuração Spring

Este projeto utiliza a configuração Spring do projeto de consola que importa (ver Secção 17.5.2).

17.6.3. As vistas da aplicação Swing

 

Este projeto utilizará duas vistas:

  • A vista de login [1] é nova. É utilizada para autenticar o utilizador que pretende utilizar a aplicação. Utiliza as seguintes classes:
    • [AbstractElectionsConnectForm], que implementa a vista;
    • [ElectionsConnectForm], que trata dos eventos da vista;
  • A vista [2] é familiar. É a que tem sido utilizada até agora. Utiliza as seguintes classes:
    • [AbstractElectionsMainForm], que implementa a vista;
    • [ElectionsMainForm], que trata dos eventos da vista;

17.6.4. A sessão

  

Quando uma aplicação Swing tem várias vistas, é necessário um mecanismo que permita que uma vista passe informações para outra vista. Utilizamos um conceito do desenvolvimento web: a sessão, que permite que as vistas associadas a um utilizador específico partilhem informações. Este conceito será implementado aqui utilizando um singleton Spring que será injetado nas duas classes que tratam os eventos para ambas as vistas:


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
... 
}
  • linha 6: a classe [UiSession] é um componente Spring;
  • linha 10: tem apenas um objetivo: armazenar o utilizador que inicia sessão com a vista [1]. A vista [2], que precisa de conhecer este utilizador, irá recuperá-lo a partir daí;

17.6.5. A classe de inicialização

  

A classe de inicialização da aplicação gráfica é a seguinte:


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);
    }
}
  • linha 5: a classe [BootElectionsSwing] estende a classe [AbstractBootElections] definida no projeto console incluído nas dependências do projeto;
  • linhas 10–13: a classe [BootElectionsSwing] irá apresentar a vista de início de sessão [ElectionsConnectForm];
  • linhas 6–8: o método [run] desta classe será executado;

17.6.6. A classe [ElectionsMainForm]

A classe [ElectionsMainForm] é aquela que anteriormente se chamava [ElectionsSwing]. Ela lida com eventos para a vista [AbstractElectionsMainForm]. O seu código evolui da seguinte forma:


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);
  }
}
  • linhas 32-33: injetar a sessão da aplicação;
  • linhas 112–119: a vista será ativada como antes, utilizando o seu método [run];
  • linha 115: o utilizador é recuperado da sessão. Este utilizador foi inserido ali pelo código na vista de início de sessão [ElectionsConnectForm]. O código é então modificado para que o primeiro parâmetro das chamadas à camada [business] seja este utilizador (linhas 63, 69-70, por exemplo);

17.6.7. A vista [AbstractElectionsConnectForm]

  

Utilize o NetBeans para criar o seguinte formulário:

 

A classe não implementará o método que lida com o clique na opção de menu [Login]. Ela chamará um método [doConnecter] declarado como abstrato, e a própria classe de visualização será declarada como abstrata. Iremos remover o método estático [main] que foi gerado automaticamente.

17.6.8. Tratamento de eventos para a vista de login

  

A classe [ElectionsConnectForm] implementa o tratamento de eventos para a vista de login da seguinte forma:


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) {
...
  }
 
}
  • linhas 73–82: o método [run], que será chamado pela classe de inicialização [BootElectionsSwing];
  • linhas 26-27: injeção de uma referência à vista n.º 2, que deve ser exibida se o utilizador for reconhecido;
  • linhas 30-31: injeção da sessão da aplicação;
  • linha 84: o método [isPageValid] faz duas coisas:
    • verifica se o nome de utilizador não está vazio (a palavra-passe pode estar vazia);
    • instancia o campo User da linha 23 com a entrada;

Tarefa: Complete o código da classe.



Tarefa: Teste a aplicação.