17. [TD]: захист веб-сервера / jSON під час виборів
Ключові слова: багаторівнева архітектура, Spring, ін’єкція залежностей, веб-сервіс / jSON захищений, клієнт / сервер
Тепер ми застосовуємо те, що вивчили в попередньому розділі, до TD щодо виборів. Архітектура буде такою:
![]() |
Процес відбуватиметься таким чином:
- написання сервера;
- написання клієнта без шару [ui], але з тестом JUnit шару [métier];
- запис з клієнта з шаром [ui];
17.1. Support
![]() | ![]() |
Проекти цього розділу знаходяться у папці [support / chap-17]. Скрипт SQL призначений для створення бази даних, необхідної для тестування.
17.2. База даних
База даних захищеного сервера тепер має містити таблиці [USERS], [ROLES] та [USERS_ROLES]:
![]() | ![]() |
Скрипт SQL для створення бази даних доступний у розділі «Документація».
17.3. Захищений сервер
Щоб налаштувати захищений сервер для виборів, достатньо скопіювати та вставити зразок проекту [intro-spring-security-server-01]:
![]() |
Що потрібно зробити: перейменуйте пакети, як показано у файлі [1].
Після цього ми змінюємо файл [pom.xml] наступним чином:
<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>
<!-- плагіни -->
<build>
...
</build>
</project>
- у рядках 23–27 замініть залежність від проєкту [intro-server-webjson-01] на залежність від проєкту [elections-webjson-metier-dao-spring-data], розглянутого в параграфі 12;
- у рядках 4–7 вкажіть характеристики нового проекту;
Залишилося змінити класи конфігурації проекту:
[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 {
// константи
final static private String[] ENTITIES_PACKAGES = { "elections.dao.entities", "elections.security.entities" };
@Bean
public String[] packagesToScan() {
return ENTITIES_PACKAGES;
}
}
Зміни внесені в наступні рядки:
- рядки 8, 9, 14: вкажіть правильні назви пакетів;
- рядок 10: імпортований клас тепер — [elections.dao.config.DaoConfig] із проєкту [elections-webjson-metier-dao-spring-data];
Примітка: як уже зазначалося, ця конфігурація працює лише в тому випадку, якщо клас [elections.dao.config.DaoConfig] має анотацію [@Configuration]. Перевірте цей момент.
[SecurityConfig]
package elections.security.config;
...
@EnableWebSecurity
@ComponentScan(basePackages = { "elections.security.service" })
@Import({ WebConfig.class, DaoConfig.class })
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
Зміни внесені в наступні рядки:
- рядок 6: змініть назву пакета;
Це все. Проведіть перше тестування, запустивши тест JUnit []:
![]() |
Запустіть клас завантаження [Boot], а потім за допомогою розширення Chrome [Advanced Rest Client] проведіть наступне тестування:
![]() |
У [1] — запит. У [3] — відповідь. У [2] заголовок HTTP є заголовком автентифікації користувача [admin, admin]: Authorization:Basic YWRtaW46YWRtaW4=
Запитайте зараз списки, що беруть участь у змаганні:
![]() |
17.4. Клієнт безпечного сервера без шару [ui]
![]() |
Скопіюйте та вставте проект [elections-ui-metier-dao-webjson] у проект [elections-metier-dao-security-webjson].
![]() |
Необхідні дії: у [1] перейменуйте пакети, якщо це необхідно, та видаліть пакети з шару [ui] і запускних класів [boot].
Тепер ми будемо діяти так, як це було зроблено в розділі 16.5.
17.4.1. Налаштування Maven
Змінюється лише частина, що стосується ідентифікації проєкту:
<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. Рефакторизація шару [métier]
![]() |
Інтерфейс [IElectionsMetier] змінюється наступним чином:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
public interface IElectionsMetier {
// аутентифікація
public void authenticate(User user);
// отримати списки учасників
public ListeElectorale[] getListesElectorales(User user);
// кількість місць, що підлягають заповненню
public int getNbSiegesAPourvoir(User user);
// виборчий бар'єр
public double getSeuilElectoral(User user);
// реєстрація результатів
public void recordResultats(User user, ListeElectorale[] listesElectorales);
// розподіл місць
public ListeElectorale[] calculerSieges(User user, ListeElectorale[] listesElectorales);
}
Першим параметром усіх методів є користувач, який бажає скористатися ресурсами захищеного сервера.
Реалізація [ElectionsMetier] змінюється наступним чином:
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;
// конфігурація виборів
private ElectionsConfig electionsConfig;
private ElectionsConfig getElectionsConfig(User user) {
if(electionsConfig!=null){
return electionsConfig;
}
// маппери jSON
ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
try {
// запит
Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse(user, "/getElectionsConfig", null),
new TypeReference<Response<ElectionsConfig>>() {
});
// помилка?
if (response.getStatus() != 0) {
// генерується 1 виняток
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);
}
}
Завдання: доповніть наведений вище код.
17.4.3. Налаштування Spring
![]() |
Клас [MetierConfig] змінюється наступним чином:
package elections.security.client.config;
...
@ComponentScan({ "elections.security.client.dao", "elections.security.client.metier" })
public class MetierConfig {
У рядку 5 потрібно оновити пакети, що підлягають обстеженню.
17.4.4. Тест JUnit для шару [métier]
![]() |
Клас тесту [Test01] змінюється наступним чином:
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 {
// шар [electionsMetier]
@Autowired
private IElectionsMetier electionsMetier;
// маппер jSON
private final ObjectMapper mapper = new ObjectMapper();
// користувачі
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() {
// створюється таблиця із 7 списків кандидатів
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);
// розраховуються місця для кожного зі списків
listes = electionsMetier.calculerSieges(admin,listes);
// перевіряються результати
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() {
// створюється таблиця із 7 списків кандидатів
ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
// фіксуються голоси
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);
// розраховуються місця, отримані кожним зі списків
listes = electionsMetier.calculerSieges(admin,listes);
// перевіряються результати
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() {
// створюється таблиця з 24 списками кандидатів, кожен із яких має 1 голос
ListeElectorale[] listes = new ListeElectorale[25];
// усі 25 списків матимуть однакову кількість голосів (4 %)
for (int i = 0; i < listes.length; i++) {
listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
}
// розподіл місць — зазвичай має бути ElectionsException
// з виборчим бар'єром у 5%
electionsMetier.calculerSieges(admin,listes);
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// створюється таблиця із 7 списків кандидатів
ListeElectorale[] listes = electionsMetier.getListesElectorales(admin);
// встановлюємо фіксовану кількість голосів
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);
// розраховуються місця, отримані кожним зі списків
listes = electionsMetier.calculerSieges(admin,listes);
// виводимо результати
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// результати записуються в базу даних
electionsMetier.recordResultats(admin,listes);
// перевіряються результати
listes = electionsMetier.getListesElectorales(admin);
// виводить результати
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());
}
}
Завдання: розберіться з цим тестом і переконайтеся, що він проходить успішно.
17.5. Клієнт захищеного сервера з шаром [console]
![]() |
У NetBeans ми відкриваємо проекти Maven [elections-metier-dao-security-webjson] та [elections-ui-metier-dao-webjson], а потім створюємо новий проект [elections-console-metier-dao-security-webjson] шляхом копіювання та вставлення проекту [elections-ui-metier-dao-webjson]:
![]() |
Більшість класів нового проєкту [3] походять із проєкту [elections-ui-metier-dao-webjson] [2], який був клієнтом незахищеного веб-сервера / jSON:
![]() |
Щоб створити новий проект Maven, виконайте такі дії:
- скопіюйте та вставте проект [elections-ui-metier-dao-webjson] у проект [elections-ui-metier-dao-security-webjson];
- у новому проєкті:
- видаліть пакети [elections.client.dao, elections.client.metier, elections.client.entities];
- у пакеті [elections.client.ui] залиште лише клас консолі [ElectionsConsole] та його інтерфейс [IElectionsUI];
- перейменуйте пакети, як зазначено, на [3];
- виправте проблеми з імпортом у різних класах за допомогою [clic droit / Fix Imports];
На цьому етапі новий проєкт містить чимало помилок.
17.5.1. Налаштування Maven
Файл [pom.xml] нового проєкту має такий вигляд:
<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>
- рядки 4–8: ідентифікатор Maven нового проєкту;
- рядки 22–26: залежність від проєкту [elections-metier-dao-security-webjson], який ми щойно створили в розділі 17.4 і який надає шари [DAO] та [métier];
17.5.2. Конфігурація Spring
![]() |
Клас [UiConfig] має такий вигляд:
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 {
// адміністратор
private final User ADMIN = new User("admin", "admin");
@Bean
private User admin() {
return ADMIN;
}
}
- рядок 8: імпортується клас [elections.security.client.config.MetierConfig] із проєкту [elections-metier-dao-security-webjson], розглянутого в розділі 17.4;
- рядок 9: оголошуються пакети, в яких знаходяться біни Spring;
- рядки 12–18: бін [admin] — це користувач [admin, admin], який використовуватиметься консольним додатком. Нагадаємо, що це єдиний користувач, уповноважений звертатися до захищеного веб-сервера /jSON;
17.5.3. Завантажувальний додаток консолі
![]() |
Клас [ElectionsConsole] розвивається наступним чином:
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() {
// списки учасників змагань
ListeElectorale[] listes;
// введення даних
try (Scanner clavier = new Scanner(System.in)) {
// запит списків кандидатів до шару [metier]
listes = electionsMetier.getListesElectorales(admin);
// здійснюється підрахунок голосів
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");
}
}
}
}
// проводиться підрахунок місць
listes=electionsMetier.calculerSieges(admin,listes);
// результати зберігаються
electionsMetier.recordResultats(admin,listes);
// сортування списків у порядку спадання кількості голосів
Arrays.sort(listes, new CompareListesElectorales());
// вони відображаються
System.out.println("\nRésultats de l'élection\n");
for (int i = 0; i < listes.length; i++) {
System.out.println(listes[i]);
}
}
// клас порівняння виборчих списків
class CompareListesElectorales implements Comparator<ListeElectorale> {
// порівняння двох списків кандидатів за кількістю голосів
@Override
public int compare(ListeElectorale listeElectorale1, ListeElectorale listeElectorale2) {
// порівнюються голоси цих двох списків
int nbVoix1 = listeElectorale1.getVoix();
int nbVoix2 = listeElectorale2.getVoix();
if (nbVoix1 < nbVoix2) {
return +1;
} else {
if (nbVoix1 > nbVoix2)
return -1;
else
return 0;
}
}
}
}
Клас ElectionsConsole майже не змінюється. Слід лише пам’ятати, що тепер методи шару [métier] вимагають як перший параметр ім’я користувача (рядки 31, 49, 51). Воно надається шляхом ін’єкції в рядках 21–22. Введений користувач — це той, хто має дозвіл на запит до веб-сервера /jSON.
Завдання: протестуйте консольну програму (не забудьте попередньо запустити захищений сервер).
17.6. Клієнт захищеного сервера з рівнем [swing]
![]() |
Ми створюємо новий проект NetBeans [elections-swing-metier-dao-security-webjson] шляхом копіювання та вставлення проекту [elections-ui-metier-dao-webjson]:
![]() |
У новому проєкті [2]:
- видаліть пакети [elections.client.dao, elections.client.metier, elections.client.entities];
- у пакеті [elections.client.ui] видаліть класи [ElectionsConsole, IElectionsUI];
- перейменуйте пакети, як зазначено, на [2];
- у пакеті [elections.security.client.swing] перейменуйте клас [AbstractElectionsSwing], який реалізує графічний інтерфейс, на [AbstractElectionsMainForm], а клас [ElectionsSwing], який реалізує обробники подійграфічного інтерфейсу — на [ElectionsMainForm];
- виправте проблеми з імпортом у різних класах за допомогою [clic droit / Fix Imports];
На цьому етапі є чимало помилок.
17.6.1. Налаштування Maven
Файл [pom.xml] змінюється наступним чином:
<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>
- рядки 4–8: ідентифікатор Maven нового проєкту;
- рядки 22–26: залежність від консольного проєкту, який ми щойно розглянули в розділі 17.5;
17.6.2. Конфігурація Spring
Цей проєкт використовує конфігурацію Spring з проєкту «console», який він імпортує (див. розділ 17.5.2).
17.6.3. Види додатка Swing
![]() |
У цьому проєкті будуть використовуватися два види:
![]() |
- вікно входу [1] є новим. Воно служить для аутентифікації користувача, який хоче скористатися додатком. У ньому використовуються класи:
- [AbstractElectionsConnectForm], що реалізує вигляд;
- [ElectionsConnectForm], що керує подіями подання;
- вигляд [2] є відомим. Саме він використовувався дотепер. Він використовує такі класи:
- [AbstractElectionsMainForm], що реалізує представлення;
- [ElectionsMainForm], що обробляє події подання;
17.6.4. Сесія
![]() |
Коли Swing-додаток має кілька представлень, потрібно знайти спосіб, щоб одне представлення могло передавати інформацію іншому. Ми використовуємо концепцію, поширену у веб-розробці: сесію, яка дозволяє представленням, пов’язаним із конкретним користувачем, обмінюватися інформацією. Ця концепція буде реалізована тут за допомогою синглтона Spring, який буде введений у два класи, що обробляють події обох представлень:
package elections.security.client.swing;
import elections.security.client.entities.User;
import org.springframework.stereotype.Component;
@Component
public class UiSession {
// зареєстрований користувач
private User user;
// методи getter та setter
...
}
- рядок 6: клас [UiSession] є компонентом Spring;
- рядок 10: він служить лише для одного — запам’ятовування користувача, який входить у систему через представлення [1]. Представлення [2], якому потрібно знати цього користувача, отримає його саме звідти;
17.6.5. Клас завантаження
![]() |
Клас завантаження графічного додатка має такий вигляд:
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);
}
}
- рядок 5: клас [BootElectionsSwing] успадковує клас [AbstractBootElections], визначений у консольному проєкті, що входить до залежностей проєкту;
- рядки 10–13: клас [BootElectionsSwing] відображатиме вікно входу [ElectionsConnectForm];
- рядки 6–8: саме метод [run] цього класу буде виконано;
17.6.6. Клас [ElectionsMainForm]
Клас [ElectionsMainForm] — це той самий клас, який раніше називався [ElectionsSwing]. Він обробляє події подання [AbstractElectionsMainForm]. Його код змінюється наступним чином:
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;
// посилання на рівень [métier]
@Autowired
private IElectionsMetier metier;
// сесія UI
@Autowired
private UiSession uiSession;
// зареєстрований користувач
private User user;
// шаблони списків JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// списки, що беруть участь у конкурсі
private ListeElectorale[] listes;
// списки, введені користувачем
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// ініціалізації
@Override
protected void init() {
// генерація компонентів батьківським класом
super.init();
// локальні ініціалізації
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
String info;
boolean erreur = false;
try {
// запитуються списки у шару [métier]
listes = metier.getListesElectorales(user);
// зв’язування назв списків із комбінованим списком jComboBoxNomsListes
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// а також параметри виборів
int nbSiegesAPourvoir = metier.getNbSiegesAPourvoir(user);
double seuilElectoral = metier.getSeuilElectoral(user);
// ініціалізуються мітки, пов’язані з цими двома видами інформації
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
// повідомлення про успішне виконання
info = "Source de données lue avec succès";
} catch (ElectionsException ex1) {
// фіксується помилка
info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
erreur = true;
} catch (RuntimeException ex2) {
// фіксується помилка
info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
erreur = true;
}
// помилка?
if (erreur) {
// відображається інформація
jTextPaneMessages.setText(info);
jTextPaneMessages.setCaretPosition(0);
return;
} else {
jTextPaneMessages.setText("");
}
// стан форми
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// вирівняти вікно по центру
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 = uiSession.getUser();
// ініціалізація вікна
init();
setVisible(true);
}
}
- рядки 32–33: введення сесії додатка;
- рядки 112–119: вигляд буде активовано, як і раніше, за допомогою його методу [run];
- рядок 115: отримуємо користувача із сесії. Він був доданий туди кодом екрану входу [ElectionsConnectForm]. Далі код змінюється таким чином, щоб першим параметром викликів шару [métier] був цей користувач (наприклад, рядки 63, 69–70);
17.6.7. Вигляд [AbstractElectionsConnectForm]
![]() |
Створіть за допомогою NetBeans таку форму:
![]() |
Клас сам не буде реалізовувати метод, що обробляє клік на пункті меню [Connexion]. Він буде викликати метод [doConnecter], оголошений абстрактним, а сам клас представлення буде оголошений абстрактним. Статичний метод [main], який було згенеровано автоматично, буде видалено.
17.6.8. Обробка подій у вікні входу
![]() |
Клас [ElectionsConnectForm] реалізує обробку подій вікна входу наступним чином:
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;
// посилання на шар [métier]
@Autowired
private IElectionsMetier metier;
// користувач увійшов у систему
private User user;
// головна форма
@Autowired
private ElectionsMainForm electionsMainForm;
// сесія UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
String info = null;
try {
if (isPageValid()) {
// аутентифікація користувача
metier.authenticate(user);
// користувач зберігається у сесії
uiSession.setUser(user);
// сторінка входу прихована
setVisible(false);
// відображається головна сторінка
electionsMainForm.run();
}
} catch (ElectionsException ex1) {
// зафіксовано помилку
info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
} catch (RuntimeException ex2) {
// зафіксовано помилку
info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
}
// помилка?
if (info != null) {
// відображається інформація
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
}
}
// ініціалізації
@Override
protected void init() {
// генерація компонентів батьківським класом
super.init();
// локальні ініціалізації
...
// центрування вікна
...
}
@Override
public void run() {
// відображення графічного інтерфейсу
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) {
...
}
}
- рядки 73–82: метод [run], який буде викликаний класом завантаження [BootElectionsSwing];
- рядки 26–27: введення посилання на вікно № 2, яке має відображатися, якщо користувач буде розпізнаний;
- рядки 30–31: введення сесії додатка;
- рядок 84: метод [isPageValid] виконує дві дії:
- перевіряє, чи ідентифікатор не порожній (пароль може бути порожнім);
- ініціює поле User у рядку 23 на основі введених даних;
![]() | ![]() |
![]() | ![]() |
Завдання: доповніть код класу.
Завдання: протестуйте додаток.































