43 Commits

Author SHA1 Message Date
Gaa56
a31ac61a9a Start traffic light threads on process initialization 2025-11-07 12:14:52 +00:00
5dc1b40c88 Merge pull request #32 from davidalves04/14-create-trafficlightthread-class
14 create trafficlightthread class
2025-11-06 13:53:12 +00:00
3117bdf332 Merge branch 'dev' into 14-create-trafficlightthread-class 2025-11-06 13:53:01 +00:00
1140c3ca48 Merge pull request #30 from davidalves04/13-create-exit-node-process
13 create exit node process
2025-11-06 13:49:21 +00:00
Gaa56
484cba1eee Update TrafficLightThread 2025-11-05 13:21:10 +00:00
Gaa56
0e5526c3f6 Merge pull request #31 from davidalves04/dev
Dev
2025-11-05 12:37:48 +00:00
David Alves
cf88db4297 Add traffic light coordination and tests
Sorry to add this on this branch ahah
2025-11-05 12:09:32 +00:00
David Alves
0960a7a141 Add ExitNodeProcess and unit tests 2025-11-05 11:54:34 +00:00
David Alves
3b4f968a59 Merge pull request #29 from davidalves04/12-implement-coordinatorgenerator-process
Coordinator Process Implementation
2025-11-03 00:02:56 +00:00
0c256ad6f5 Fix Intersection Destination - Doubled Advance 2025-11-02 23:56:54 +00:00
340e436063 Merge branch 'dev' into 12-implement-coordinatorgenerator-process 2025-11-02 23:21:36 +00:00
1684a6713e Implementation of the Coordinator Process 2025-11-02 23:17:15 +00:00
22a7081ade Merge pull request #28 from davidalves04/10-create-network-communication-classes
Fix Serialization
2025-11-02 22:39:38 +00:00
3b699556db Merge branch 'dev' into 10-create-network-communication-classes 2025-11-02 22:39:26 +00:00
Gaa56
d078808486 Update SocketConnection 2025-10-30 19:25:27 +00:00
Gaa56
98581b562d Merge pull request #27 from davidalves04/9-design-message-protocol-specification
#10 Req
2025-10-30 18:44:54 +00:00
Gaa56
4710c96450 Create TrafficLightThread Class 2025-10-30 18:06:02 +00:00
f9644bd18c Merge pull request #26 from davidalves04/dev
#12 Req.
2025-10-30 16:09:04 +00:00
David Alves
c6b710ac52 Merge pull request #25 from davidalves04/11-convert-intersection-to-standalone-process
11 convert intersection to standalone process
2025-10-30 16:00:05 +00:00
David Alves
dc4f567e1f Move vehicle route advancement to intersection arrival 2025-10-30 15:57:58 +00:00
David Alves
db5e01021a Refactor IntersectionProcess and add unit tests 2025-10-30 10:41:17 +00:00
David Alves
dab0651dbd Corrected directions 2025-10-29 22:36:58 +00:00
David Alves
4772add574 Merge pull request #24 from davidalves04/dev
Dev
2025-10-27 23:04:15 +00:00
David Alves
ae27115791 Merge pull request #23 from davidalves04/11-convert-intersection-to-standalone-process
Create IntersectionProcess main class
2025-10-27 22:58:55 +00:00
David Alves
684fb408ef Create IntersectionProcess main class 2025-10-27 22:53:37 +00:00
David Alves
d057adeab3 Revert "Enunciado uploaded"
This reverts commit be4e7f66d6.
2025-10-27 22:52:19 +00:00
David Alves
be4e7f66d6 Enunciado uploaded 2025-10-27 18:03:17 +00:00
fd26063f6e Merge pull request #22 from davidalves04/10-create-network-communication-classes
Create network communication classes
2025-10-27 12:29:22 +00:00
Gaa56
d8b59cc502 Deleted MessageSerializer 2025-10-27 09:18:33 +00:00
Gaa56
06c34a198a Removed MessageSerializer 2025-10-27 09:15:33 +00:00
Gaa56
1524188b29 Add connection retry logic 2025-10-26 17:00:34 +00:00
Gaa56
bc1a8da160 Create MessageSerializer utility 2025-10-25 18:00:58 +01:00
Gaa56
96903e4b7c SocketConnection 2025-10-25 17:43:25 +01:00
Gaa56
6c5eab0e72 Create SocketConnection wrapper class 2025-10-25 17:41:55 +01:00
23f7a74798 Add dependency build to CI job 2025-10-24 20:20:15 +01:00
d7dec0d73e Merge pull request #21 from davidalves04/9-design-message-protocol-specification
Mmessage protocol specification
2025-10-24 20:12:18 +01:00
David Alves
534a880e3e Remove unused supports method from MessageSerializer 2025-10-24 12:02:03 +01:00
David Alves
ba3233eae1 Java serialization removed 2025-10-23 22:44:25 +01:00
David Alves
d20040835c README 2025-10-23 20:28:43 +01:00
David Alves
2399b4b472 Delete main/docs directory 2025-10-23 20:22:53 +01:00
David Alves
974debf7db Design serialization format
JSON
2025-10-23 20:08:26 +01:00
Gaa56
3fe467a2a3 Create MessageProtocol interface 2025-10-22 19:19:28 +01:00
David Alves
af9b091e76 Define message types 2025-10-22 18:43:49 +01:00
23 changed files with 4500 additions and 3 deletions

620
README.md Normal file
View File

@@ -0,0 +1,620 @@
# Sistema de Simulação de Tráfego Distribuído
Sistema distribuído de simulação de tráfego.
---
## Índice
- [Visão Geral](#visão-geral)
- [Arquitetura](#arquitetura)
- [Protocolo de Comunicação](#protocolo-de-comunicação)
- [Estrutura do Projeto](#estrutura-do-projeto)
- [Instalação e Execução](#instalação-e-execução)
- [Documentação](#documentação)
- [Desenvolvimento](#desenvolvimento)
---
## Visão Geral
Este projeto implementa uma simulação distribuída de tráfego veicular numa rede de cruzamentos. O sistema utiliza:
- **Processos independentes** para cada cruzamento
- **Threads** para controlar os semáforos dentro de cada cruzamento
- **Comunicação via sockets** para transferência de veículos entre cruzamentos
- **Simulação de eventos discretos** (DES) para gerir o tempo de simulação
### Características Principais
- Simulação determinística e reproduzível
- Comunicação assíncrona entre processos
- Protocolo de mensagens baseado em JSON
- Dashboard em tempo real (planeado)
- Estatísticas detalhadas de desempenho
---
## Arquitetura
### Visão Geral do Sistema
```
┌─────────────────────────────────────────────────────────────────┐
│ SISTEMA DISTRIBUÍDO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Coordenador │ ────────────────────────>│ Dashboard │ │
│ │ / Gerador │ │ │
│ └──────┬───────┘ └──────▲───────┘ │
│ │ │ │
│ │ Gera veículos Stats │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────────────────────────────────────┴──────┐ │
│ │ Rede de Cruzamentos (Processos) │ │
│ │ │ │
│ │ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ │Cr1 │◄───────►│Cr2 │◄───────►│Cr3 │ │ │
│ │ └─┬──┘ └─┬──┘ └─┬──┘ │ │
│ │ │ │ │ │ │
│ │ │ ┌────▼────┐ │ │ │
│ │ └────────►│ Cr4 │◄────────┘ │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ ┌────▼────┐ │ │
│ │ │ Cr5 │ │ │
│ │ └────┬────┘ │ │
│ └───────────────────┼─────────────────────────────────────┤ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ Nó de Saída │ │ │
│ │ (S) │ │ │
│ └──────────────┘ │ │
│ │ │
└────────────────────────────────────────────────────────────┘ │
```
### Componentes
1. **Coordenador/Gerador**: Gera veículos e injeta no sistema
2. **Cruzamentos (Cr1-Cr5)**: Processos independentes que gerem tráfego local
3. **Nó de Saída (S)**: Recolhe estatísticas de veículos que saem do sistema
4. **Dashboard Server**: Agrega e exibe dados em tempo real
---
## Protocolo de Comunicação
### Formato de Serialização: JSON (Gson)
O sistema utiliza JSON como formato de serialização por ser mais rápido, seguro e legível que a serialização em Java.
### Estrutura de Mensagens
Todas as mensagens seguem o formato base:
```json
{
"messageId": "uuid",
"type": "MESSAGE_TYPE",
"senderId": "sender_id",
"destinationId": "destination_id",
"timestamp": 1729595234567,
"payload": { ... }
}
```
### Tipos de Mensagens
#### 1. VEHICLE_TRANSFER
Transfere um veículo entre cruzamentos.
**Estrutura:**
```json
{
"messageId": "a3c5e7f9-1234-5678-90ab-cdef12345678",
"type": "VEHICLE_TRANSFER",
"senderId": "Cr1",
"destinationId": "Cr2",
"timestamp": 1729595234567,
"payload": {
"id": "V123",
"type": "LIGHT",
"entryTime": 15.7,
"route": ["Cr1", "Cr2", "Cr5", "S"],
"currentRouteIndex": 1,
"totalWaitingTime": 3.2,
"totalCrossingTime": 1.8
}
}
```
**Fluxo:**
1. Veículo completa travessia no Cr1
2. Cr1 serializa mensagem VEHICLE_TRANSFER
3. Envia para Cr2 via socket
4. Cr2 desserializa e adiciona veículo à fila
#### 2. STATS_UPDATE
Envia estatísticas de um cruzamento para o Dashboard.
**Estrutura:**
```json
{
"messageId": "b4d6e8f0-2345-6789-01bc-def123456789",
"type": "STATS_UPDATE",
"senderId": "Cr3",
"destinationId": "Dashboard",
"timestamp": 1729595234789,
"payload": {
"intersectionId": "Cr3",
"queueLengths": {
"North": 5,
"South": 3,
"East": 7,
"West": 2
},
"vehiclesProcessed": 142,
"averageWaitTime": 4.5,
"currentTime": 123.45
}
}
```
**Frequência:** A cada 10 segundos (configurável)
#### 3. VEHICLE_EXIT
Notifica quando um veículo sai do sistema.
**Estrutura:**
```json
{
"messageId": "c5e7f9a1-3456-7890-12bc-def123456789",
"type": "VEHICLE_EXIT",
"senderId": "Cr5",
"destinationId": "ExitNode",
"timestamp": 1729595234890,
"payload": {
"id": "V123",
"type": "LIGHT",
"entryTime": 15.7,
"exitTime": 45.2,
"totalSystemTime": 29.5,
"totalWaitingTime": 8.3,
"totalCrossingTime": 4.8,
"routeTaken": ["Cr1", "Cr2", "Cr5", "S"]
}
}
```
#### 4. HEARTBEAT
Mantém a ligação ativa e monitoriza a saúde dos processos.
**Estrutura:**
```json
{
"messageId": "d6e8f0a2-4567-8901-23cd-ef1234567890",
"type": "HEARTBEAT",
"senderId": "Cr1",
"destinationId": "Coordinator",
"timestamp": 1729595235000,
"payload": {
"status": "RUNNING",
"uptime": 120.5,
"vehiclesInQueue": 12
}
}
```
**Frequência:** A cada 5 segundos
#### 5. LIGHT_CHANGE
Notifica mudança de estado de semáforo (para logging/debugging).
**Estrutura:**
```json
{
"messageId": "e7f9a1b3-5678-9012-34de-f12345678901",
"type": "LIGHT_CHANGE",
"senderId": "Cr1-North",
"destinationId": "Dashboard",
"timestamp": 1729595235100,
"payload": {
"lightId": "Cr1-North",
"previousState": "RED",
"newState": "GREEN",
"queueSize": 5
}
}
```
### Tipos de Veículos
```json
{
"BIKE": {
"probability": 0.20,
"crossingTime": 1.5
},
"LIGHT": {
"probability": 0.60,
"crossingTime": 2.0
},
"HEAVY": {
"probability": 0.20,
"crossingTime": 4.0
}
}
```
### Estados dos Semáforos
```
RED → Veículos aguardam na fila
GREEN → Veículos podem atravessar
```
### Exemplo de Comunicação Completa
```
Tempo Processo Ação Mensagem
------ --------- ------------------------------------- ------------------
15.7s Gerador Gera veículo V123 -
15.7s Gerador → Injeta V123 em Cr1 VEHICLE_TRANSFER
18.2s Cr1 V123 inicia travessia -
20.2s Cr1 V123 completa travessia -
20.2s Cr1 → Cr2 Transfere V123 para Cr2 VEHICLE_TRANSFER
23.5s Cr2 V123 inicia travessia -
25.5s Cr2 V123 completa travessia -
25.5s Cr2 → Cr5 Transfere V123 para Cr5 VEHICLE_TRANSFER
28.0s Cr5 V123 inicia travessia -
30.0s Cr5 V123 completa travessia -
30.0s Cr5 → Exit V123 sai do sistema VEHICLE_EXIT
30.0s Exit → Dash Estatísticas de V123 STATS_UPDATE
```
---
## Estrutura do Projeto
```
Trabalho-Pratico-SD/
├── README.md # Este ficheiro
├── TODO.md # Plano de desenvolvimento
├── main/
│ ├── pom.xml # Configuração do Maven
│ ├── docs/
│ │ ├── README.md # Índice da documentação
│ │ ├── SERIALIZATION_SPECIFICATION.md
│ │ ├── SERIALIZATION_DECISION.md
│ │ ├── SERIALIZATION_SUMMARY.md
│ │ └── SERIALIZATION_ARCHITECTURE.md
│ ├── src/
│ │ ├── main/java/sd/
│ │ │ ├── Entry.java # Ponto de entrada
│ │ │ ├── config/
│ │ │ │ └── SimulationConfig.java
│ │ │ ├── engine/
│ │ │ │ └── SimulationEngine.java
│ │ │ ├── model/
│ │ │ │ ├── Event.java
│ │ │ │ ├── EventType.java
│ │ │ │ ├── Intersection.java
│ │ │ │ ├── Message.java # Estrutura de mensagens
│ │ │ │ ├── MessageType.java # Tipos de mensagens
│ │ │ │ ├── TrafficLight.java
│ │ │ │ ├── Vehicle.java
│ │ │ │ └── VehicleType.java
│ │ │ ├── serialization/ # Sistema de serialização
│ │ │ │ ├── MessageSerializer.java
│ │ │ │ ├── SerializationException.java
│ │ │ │ ├── JsonMessageSerializer.java
│ │ │ │ ├── SerializerFactory.java
│ │ │ │ ├── SerializationExample.java
│ │ │ │ └── README.md
│ │ │ └── util/
│ │ │ ├── RandomGenerator.java
│ │ │ ├── StatisticsCollector.java
│ │ │ └── VehicleGenerator.java
│ │ └── test/java/
│ │ ├── SimulationTest.java
│ │ └── sd/serialization/
│ │ └── SerializationTest.java
│ └── target/ # Ficheiros compilados
└── .vscode/ # Configuração do VS Code
```
---
## Instalação e Execução
### Pré-requisitos
- **Java 17** ou superior
- **Maven 3.8+**
- **Git**
### Instalação
```bash
# Clonar o repositório
git clone https://github.com/davidalves04/Trabalho-Pratico-SD.git
cd Trabalho-Pratico-SD/main
# Compilar o projeto
mvn clean compile
# Executar os testes
mvn test
```
### Execução
#### Simulação Básica (Single Process)
```bash
mvn exec:java -Dexec.mainClass="sd.Entry"
```
#### Exemplo de Serialização
```bash
mvn exec:java -Dexec.mainClass="sd.serialization.SerializationExample"
```
#### Configuração
Editar `src/main/resources/simulation.properties`:
```properties
# Duração da simulação (segundos)
simulation.duration=60.0
# Modelo de chegada: FIXED ou POISSON
arrival.model=POISSON
# Taxa de chegada (veículos/segundo)
arrival.rate=0.5
# Intervalo de atualização de estatísticas (segundos)
stats.update.interval=10.0
# Distribuição de tipos de veículos
vehicle.type.bike.probability=0.20
vehicle.type.light.probability=0.60
vehicle.type.heavy.probability=0.20
# Tempos de travessia por tipo (segundos)
vehicle.type.bike.crossing.time=1.5
vehicle.type.light.crossing.time=2.0
vehicle.type.heavy.crossing.time=4.0
```
---
## Documentação
### Documentação de Serialização
A documentação completa sobre o protocolo de serialização está disponível em:
- **[Índice Completo](./main/docs/README.md)** - Navegação da documentação
- **[Especificação](./main/docs/SERIALIZATION_SPECIFICATION.md)** - Design detalhado
- **[Guia de Decisão](./main/docs/SERIALIZATION_DECISION.md)** - Porquê JSON?
- **[Resumo](./main/docs/SERIALIZATION_SUMMARY.md)** - Estado de implementação
- **[Arquitetura](./main/docs/SERIALIZATION_ARCHITECTURE.md)** - Diagramas visuais
### Guias de Utilização
- **[Serialization README](./main/src/main/java/sd/serialization/README.md)** - Como utilizar os serializers
### Exemplos de Código
```java
// Criar serializer
MessageSerializer serializer = SerializerFactory.createDefault();
// Serializar mensagem
Vehicle vehicle = new Vehicle("V123", VehicleType.LIGHT, 10.5, route);
Message message = new Message(
MessageType.VEHICLE_TRANSFER,
"Cr1",
"Cr2",
vehicle
);
byte[] data = serializer.serialize(message);
// Enviar via socket
outputStream.write(data);
// Receber e desserializar
byte[] received = inputStream.readAllBytes();
Message msg = serializer.deserialize(received, Message.class);
Vehicle v = msg.getPayloadAs(Vehicle.class);
```
---
## Desenvolvimento
### Estado do Projeto
| Componente | Estado | Notas |
|------------|--------|-------|
| Modelo de Dados | Completo | Vehicle, Message, Event, etc. |
| Simulação DES | Completo | Single-process funcional |
| Serialização | Completo | JSON e Java implementados |
| Testes | 14/14 | Suite de serialização |
| Processos Distribuídos | Planeado | Próxima etapa |
| Comunicação Sockets | Planeado | Em design |
| Dashboard | Planeado | UI web |
### Roteiro de Desenvolvimento
#### Fase 1: Fundações (Concluído)
- Modelação de classes
- Simulação DES single-process
- Design de protocolo de serialização
- Implementação JSON/Java serialization
- Testes unitários
#### Fase 2: Distribuição (Em Curso)
- Implementar comunicação via sockets
- Separar cruzamentos em processos
- Implementar threads de semáforos
- Testar comunicação entre processos
#### Fase 3: Dashboard e Monitorização
- Dashboard server
- UI web em tempo real
- Visualização de estatísticas
- Logs estruturados
#### Fase 4: Optimização e Análise
- Testes de carga
- Análise de diferentes políticas
- Recolha de métricas
- Relatório final
### Executar Testes
```bash
# Todos os testes
mvn test
# Apenas testes de serialização
mvn test -Dtest=SerializationTest
# Com relatório de cobertura
mvn test jacoco:report
```
### Contribuir
1. Fork o projeto
2. Criar uma branch para a funcionalidade (`git checkout -b feature/MinhaFuncionalidade`)
3. Commit das alterações (`git commit -m 'Adiciona MinhaFuncionalidade'`)
4. Push para a branch (`git push origin feature/MinhaFuncionalidade`)
5. Abrir um Pull Request
---
## Métricas de Desempenho
### Serialização
| Formato | Tamanho | Latência | Throughput |
|---------|---------|----------|------------|
| JSON | 300 bytes | 40.79 μs | ~24k msgs/s |
| Java | 657 bytes | 33.34 μs | ~30k msgs/s |
**Conclusão**: JSON é 54% menor com overhead desprezível (7 μs)
### Simulação
- **Veículos gerados/s**: ~0.5-1.0 (configurável)
- **Throughput**: ~0.2 veículos/s (saída)
- **Tempo de execução**: 140ms para 60s de simulação
- **Overhead**: < 0.25% do tempo simulado
---
## Protocolo de Mensagens - Resumo
### Formato Base
```
+------------------+
| Message Header |
|------------------|
| messageId | UUID único
| type | Enum MessageType
| senderId | ID do processo remetente
| destinationId | ID do processo destino (null = broadcast)
| timestamp | Tempo de criação (ms)
+------------------+
| Payload |
|------------------|
| Object | Dados específicos do tipo de mensagem
+------------------+
```
### Serialização
- **Formato**: JSON (UTF-8)
- **Biblioteca**: Gson 2.10.1
- **Codificação**: UTF-8
- **Compressão**: Opcional (gzip)
### Transporte
- **Protocolo**: TCP/IP
- **Porta base**: 5000+ (configurável)
- **Timeout**: 30s
- **Keep-alive**: Heartbeat a cada 5s
---
## Segurança
### Considerações
1. **Validação de Mensagens**
- Verificar tipos esperados
- Validar intervalos de valores
- Rejeitar mensagens malformadas
2. **Autenticação** (Planeado)
- Autenticação baseada em token
- Whitelist de processos
3. **Encriptação** (Opcional)
- TLS/SSL para produção
- Não necessário para ambiente de desenvolvimento local
---
## Licença
Este projeto é desenvolvido para fins académicos no âmbito da disciplina de Sistemas Distribuídos (SD) do Instituto Politécnico do Porto.
---
## Equipa
**Instituição**: Instituto Politécnico do Porto
**Curso**: Sistemas Distribuídos
**Ano Letivo**: 2025-2026 ( Semestre)
---
## Suporte
Para questões ou problemas:
1. Consultar a [documentação](./main/docs/README.md)
2. Ver [exemplos de código](./main/src/main/java/sd/serialization/SerializationExample.java)
3. Executar testes: `mvn test`
4. Abrir issue no GitHub
---
## Ligações Úteis
- [Documentação do Projeto](./main/docs/README.md)
- [Plano de Desenvolvimento](./TODO.md)
- [Especificação de Serialização](./main/docs/SERIALIZATION_SPECIFICATION.md)
- [Guia de Serialização](./main/src/main/java/sd/serialization/README.md)
---
**Última actualização**: 23 de outubro de 2025
**Versão**: 1.0.0
**Estado**: Em Desenvolvimento Activo

View File

@@ -22,6 +22,47 @@
<version>5.10.0</version> <version>5.10.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Gson for JSON serialization -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies> </dependencies>
<build>
<plugins>
<!-- Maven Exec Plugin for running examples -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>sd.Entry</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>sd.Entry</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -0,0 +1,382 @@
package sd;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import sd.config.SimulationConfig;
import sd.coordinator.SocketClient;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection;
import sd.serialization.SerializationException;
/**
* Processo responsável pelo nó de saída do sistema de simulação de tráfego distribuído.
*
* Este processo representa o ponto final ("S") onde os veículos completam as suas rotas.
* As suas principais responsabilidades são:
* - Receber veículos que terminam a sua rota vindos das interseções
* - Calcular e agregar estatísticas finais dos veículos
* - Enviar estatísticas periódicas para o dashboard
* - Gerar relatórios finais ao terminar a simulação
*/
public class ExitNodeProcess {
private final SimulationConfig config;
private ServerSocket serverSocket;
private final ExecutorService connectionHandlerPool;
/** Flag para controlar a execução do processo (volatile para visibilidade entre threads) */
private volatile boolean running;
/** Counter de veículos que completaram a rota */
private int totalVehiclesReceived;
/** Soma dos tempos no sistema de todos os veículos */
private double totalSystemTime;
/** Soma dos tempos de espera de todos os veículos */
private double totalWaitingTime;
/** Soma dos tempos de travessia de todos os veículos */
private double totalCrossingTime;
/** Contagem de veículos por tipo */
private final Map<VehicleType, Integer> vehicleTypeCount;
/** Tempo total de espera acumulado por tipo de veículo */
private final Map<VehicleType, Double> vehicleTypeWaitTime;
/** Socket para comunicação com o dashboard */
private SocketClient dashboardClient;
/**
* Método para iniciar o processo
*
* @param args Argumentos da linha de comandos. Se fornecido, args[0] deve ser
* o caminho para um ficheiro de configuração personalizado.
*/
public static void main(String[] args) {
System.out.println("=".repeat(60));
System.out.println("EXIT NODE PROCESS");
System.out.println("=".repeat(60));
try {
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
System.out.println("Loading configuration from: " + configFile);
SimulationConfig config = new SimulationConfig(configFile);
ExitNodeProcess exitNode = new ExitNodeProcess(config);
System.out.println("\n" + "=".repeat(60));
exitNode.initialize();
System.out.println("\n" + "=".repeat(60));
exitNode.start();
} catch (IOException e) {
System.err.println("Failed to start exit node: " + e.getMessage());
System.exit(1);
} catch (Exception e) {
System.err.println("Exit node error: " + e.getMessage());
System.exit(1);
}
}
/**
* Constrói um novo processo de nó de saída.
*
* Inicializa todas as estruturas de dados necessárias para recolher estatísticas
* e configura o pool de threads para processar as ligações concorrentes.
*
* @param config Configuração da simulação contendo portas e endereços dos serviços
*/
public ExitNodeProcess(SimulationConfig config) {
this.config = config;
this.connectionHandlerPool = Executors.newCachedThreadPool();
this.running = false;
this.totalVehiclesReceived = 0;
this.totalSystemTime = 0.0;
this.totalWaitingTime = 0.0;
this.totalCrossingTime = 0.0;
this.vehicleTypeCount = new HashMap<>();
this.vehicleTypeWaitTime = new HashMap<>();
// Inicializa os counters para cada tipo de veículo
for (VehicleType type : VehicleType.values()) {
vehicleTypeCount.put(type, 0);
vehicleTypeWaitTime.put(type, 0.0);
}
System.out.println("Exit node initialized");
System.out.println(" - Exit port: " + config.getExitPort());
System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort());
}
/**
* Inicializa o processo de ligação ao dashboard.
*
* Tenta conectar-se ao dashboard. Se a ligação falhar, o processo
* continua a funcionar normalmente, mas sem enviar estatísticas.
*
*/
public void initialize() {
System.out.println("Connecting to dashboard...");
try {
String host = config.getDashboardHost();
int port = config.getDashboardPort();
dashboardClient = new SocketClient("Dashboard", host, port);
dashboardClient.connect();
System.out.println("Successfully connected to dashboard");
} catch (IOException e) {
System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
System.err.println("Exit node will continue without dashboard connection");
}
}
/**
* Inicia o socket e começa a aceitar ligações.
*
* Este é o loop principal do processo que:
* 1. Cria um socket na porta definida
* 2. Aguarda pelas ligações das interseções
* 3. Delega cada ligação a uma thread da pool para processamento assíncrono
*
* @throws IOException Se o socket não puder ser criado ou houver erro na aceitação
*/
public void start() throws IOException {
int port = config.getExitPort();
serverSocket = new ServerSocket(port);
running = true;
System.out.println("Exit node started on port " + port);
System.out.println("Waiting for vehicles...\n");
while (running) {
try {
Socket clientSocket = serverSocket.accept();
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
} catch (IOException e) {
if (running) {
System.err.println("Error accepting connection: " + e.getMessage());
}
}
}
}
/**
* Processa uma ligação recebida de uma interseção.
*
* Mantém a ligação aberta e processa continuamente mensagens do tipo
* VEHICLE_TRANSFER. Cada mensagem representa um veículo que chegou ao nó de saída.
*
* @param clientSocket Socket da ligação estabelecida com a interseção
*/
private void handleIncomingConnection(Socket clientSocket) {
try (SocketConnection connection = new SocketConnection(clientSocket)) {
System.out.println("New connection accepted from " +
clientSocket.getInetAddress().getHostAddress());
while (running && connection.isConnected()) {
try {
MessageProtocol message = connection.receiveMessage();
if (message.getType() == MessageType.VEHICLE_TRANSFER) {
Vehicle vehicle = (Vehicle) message.getPayload();
processExitingVehicle(vehicle);
}
} catch (ClassNotFoundException e) {
System.err.println("Unknown message type received: " + e.getMessage());
}
}
} catch (IOException e) {
if (running) {
System.err.println("Connection error: " + e.getMessage());
}
}
}
/**
* Processa um veículo que chegou ao nó de saída.
*
* Método sincronizado para garantir thread-safety ao atualizar as estatísticas.
* Calcula as métricas finais do veículo e atualiza:
* - Counters globais;
* - Estatísticas por tipo de veículo;
* - Faz update ao dashboard a cada 10 veículos.
*
* @param vehicle Veículo que completou a sua rota
*/
private synchronized void processExitingVehicle(Vehicle vehicle) {
totalVehiclesReceived++;
double systemTime = vehicle.getTotalTravelTime(getCurrentTime());
double waitTime = vehicle.getTotalWaitingTime();
double crossingTime = vehicle.getTotalCrossingTime();
totalSystemTime += systemTime;
totalWaitingTime += waitTime;
totalCrossingTime += crossingTime;
VehicleType type = vehicle.getType();
vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);
System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs)%n",
vehicle.getId(), vehicle.getType(), systemTime, waitTime);
if (totalVehiclesReceived % 10 == 0) {
sendStatsToDashboard();
}
}
/**
* Obtém o tempo atual da simulação em segundos.
*
* @return Tempo atual em segundos desde "epoch"
*
* "Epoch" é um ponto de referência temporal Unix (1 de janeiro de 1970).
* Este método retorna os segundos decorridos desde esse momento.
*/
private double getCurrentTime() {
return System.currentTimeMillis() / 1000.0;
}
/**
* Envia as estatísticas para o dashboard.
*
* Prepara e envia uma mensagem STATS_UPDATE com:
* - O total de veículos processados;
* - A média dos tempos (sistema, espera, travessia);
* - As contagens e médias por cada tipo de veículo.
*
*/
private void sendStatsToDashboard() {
if (dashboardClient == null || !dashboardClient.isConnected()) {
return;
}
try {
Map<String, Object> stats = new HashMap<>();
stats.put("totalVehicles", totalVehiclesReceived);
stats.put("avgSystemTime", totalVehiclesReceived > 0 ? totalSystemTime / totalVehiclesReceived : 0.0);
stats.put("avgWaitingTime", totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0);
stats.put("avgCrossingTime", totalVehiclesReceived > 0 ? totalCrossingTime / totalVehiclesReceived : 0.0);
Map<String, Integer> typeCounts = new HashMap<>();
Map<String, Double> typeAvgWait = new HashMap<>();
for (VehicleType type : VehicleType.values()) {
int count = vehicleTypeCount.get(type);
typeCounts.put(type.name(), count);
if (count > 0) {
typeAvgWait.put(type.name(), vehicleTypeWaitTime.get(type) / count);
}
}
stats.put("vehicleTypeCounts", typeCounts);
stats.put("vehicleTypeAvgWait", typeAvgWait);
Message message = new Message(MessageType.STATS_UPDATE, "ExitNode", "Dashboard", stats);
dashboardClient.send(message);
System.out.printf("[Exit] Sent stats to dashboard (total=%d, avg_wait=%.2fs)%n",
totalVehiclesReceived, totalWaitingTime / totalVehiclesReceived);
} catch (SerializationException | IOException e) {
System.err.println("Failed to send stats to dashboard: " + e.getMessage());
}
}
/**
* Termina o processo
*
* Executa a seguinte sequência:
* Imprime as estatísticas finais no terminal;
* Envia a última atualização de estatísticas ao dashboard;
* Fecha o socket;
* Aguarda pela finalização das threads;
* Fecha a ligação com o dashboard;
*/
public void shutdown() {
System.out.println("\n[Exit] Shutting down...");
running = false;
printFinalStatistics();
sendStatsToDashboard();
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
System.err.println("Error closing server socket: " + e.getMessage());
}
connectionHandlerPool.shutdown();
try {
if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
connectionHandlerPool.shutdownNow();
}
} catch (InterruptedException e) {
connectionHandlerPool.shutdownNow();
}
if (dashboardClient != null) {
dashboardClient.close();
}
System.out.println("[Exit] Shutdown complete.");
System.out.println("=".repeat(60));
}
/**
* Imprime as estatísticas finais detalhadas no terminal
*
* Gera um relatório com:
* Total de veículos que completaram a rota;
* Médias de tempo no sistema, espera e travessia;
* Distribuição e médias pelo tipo de veículo (BIKE, LIGHT, HEAVY);
*
* Este método é chamado durante o shutdown para fornecer um resumo
* da simulação antes de terminar o processo.
*/
private void printFinalStatistics() {
System.out.println("\n=== EXIT NODE STATISTICS ===");
System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesReceived);
if (totalVehiclesReceived > 0) {
System.out.printf("%nAVERAGE METRICS:%n");
System.out.printf(" System Time: %.2f seconds%n", totalSystemTime / totalVehiclesReceived);
System.out.printf(" Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesReceived);
System.out.printf(" Crossing Time: %.2f seconds%n", totalCrossingTime / totalVehiclesReceived);
}
System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
for (VehicleType type : VehicleType.values()) {
int count = vehicleTypeCount.get(type);
if (count > 0) {
double percentage = (count * 100.0) / totalVehiclesReceived;
double avgWait = vehicleTypeWaitTime.get(type) / count;
System.out.printf(" %s: %d (%.1f%%), Avg Wait: %.2fs%n",
type, count, percentage, avgWait);
}
}
}
}

View File

@@ -0,0 +1,448 @@
package sd;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import sd.config.SimulationConfig;
import sd.engine.TrafficLightThread;
import sd.model.Intersection;
import sd.model.MessageType;
import sd.model.TrafficLight;
import sd.model.Vehicle;
import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection;
/**
* Main class for an Intersection Process in the distributed traffic simulation.
* * Each IntersectionProcess runs as an independent Java application (JVM instance)
* representing one of the five intersections (Cr1-Cr5) in the network.
*/
public class IntersectionProcess {
private final String intersectionId;
private final SimulationConfig config;
private final Intersection intersection;
private ServerSocket serverSocket;
private final Map<String, SocketConnection> outgoingConnections;
private final ExecutorService connectionHandlerPool;
private final ExecutorService trafficLightPool;
private volatile boolean running; //Quando uma thread escreve um valor volatile, todas as outras
//threads veem a mudança imediatamente.
// Traffic Light Coordination
/**
* Lock to ensure mutual exclusion between traffic lights.
* Only one traffic light can be green at any given time within this intersection.
*/
private final Lock trafficCoordinationLock;
/**
* Tracks which direction currently has the green light.
* null means no direction is currently green (all are red).
*/
private volatile String currentGreenDirection;
/**
* Constructs a new IntersectionProcess.
*
* @param intersectionId The ID of this intersection (e.g., "Cr1").
* @param configFilePath Path to the simulation.properties file.
* @throws IOException If configuration cannot be loaded.
*/
public IntersectionProcess(String intersectionId, String configFilePath) throws IOException {
this.intersectionId = intersectionId;
this.config = new SimulationConfig(configFilePath);
this.intersection = new Intersection(intersectionId);
this.outgoingConnections = new HashMap<>();
this.connectionHandlerPool = Executors.newCachedThreadPool();
this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions
this.running = false;
this.trafficCoordinationLock = new ReentrantLock();
this.currentGreenDirection = null;
System.out.println("=".repeat(60));
System.out.println("INTERSECTION PROCESS: " + intersectionId);
System.out.println("=".repeat(60));
}
public void initialize() {
System.out.println("\n[" + intersectionId + "] Initializing intersection...");
createTrafficLights();
configureRouting();
System.out.println("[" + intersectionId + "] Initialization complete.");
}
/**
* Creates traffic lights for this intersection based on its physical connections.
* Each intersection has different number and directions of traffic lights
* according to the network topology.
*/
private void createTrafficLights() {
System.out.println("\n[" + intersectionId + "] Creating traffic lights...");
String[] directions = new String[0];
switch (intersectionId) {
case "Cr1":
directions = new String[]{"East", "South"};
break;
case "Cr2":
directions = new String[]{"West", "East", "South"};
break;
case "Cr3":
directions = new String[]{"West", "South"};
break;
case "Cr4":
directions = new String[]{"East"};
break;
case "Cr5":
directions = new String[]{"East"};
break;
}
for (String direction : directions) {
double greenTime = config.getTrafficLightGreenTime(intersectionId, direction);
double redTime = config.getTrafficLightRedTime(intersectionId, direction);
TrafficLight light = new TrafficLight(
intersectionId + "-" + direction,
direction,
greenTime,
redTime
);
intersection.addTrafficLight(light);
System.out.println(" Created traffic light: " + direction +
" (Green: " + greenTime + "s, Red: " + redTime + "s)");
}
}
private void configureRouting() {
System.out.println("\n[" + intersectionId + "] Configuring routing...");
switch (intersectionId) {
case "Cr1":
intersection.configureRoute("Cr2", "East");
intersection.configureRoute("Cr4", "South");
break;
case "Cr2":
intersection.configureRoute("Cr1", "West");
intersection.configureRoute("Cr3", "East");
intersection.configureRoute("Cr5", "South");
break;
case "Cr3":
intersection.configureRoute("Cr2", "West");
intersection.configureRoute("S", "South");
break;
case "Cr4":
intersection.configureRoute("Cr5", "East");
break;
case "Cr5":
intersection.configureRoute("S", "East");
break;
default:
System.err.println(" Error: unknown intersection ID: " + intersectionId);
}
System.out.println(" Routing configured.");
}
/**
* Starts all traffic light threads.
*/
private void startTrafficLights() {
System.out.println("\n[" + intersectionId + "] Starting traffic light threads...");
for (TrafficLight light : intersection.getTrafficLights()) {
TrafficLightThread lightTask = new TrafficLightThread(light, this, config);
trafficLightPool.submit(lightTask);
System.out.println(" Started thread for: " + light.getDirection());
}
}
/**
* Sends a vehicle to its next destination via socket connection.
*
* @param vehicle The vehicle that has crossed this intersection.
*/
public void sendVehicleToNextDestination(Vehicle vehicle) {
String nextDestination = vehicle.getCurrentDestination();
try {
// Get or create connection to next destination
SocketConnection connection = getOrCreateConnection(nextDestination);
// Create and send message
MessageProtocol message = new VehicleTransferMessage(
intersectionId,
nextDestination,
vehicle
);
connection.sendMessage(message);
System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() +
" to " + nextDestination);
// Note: vehicle route is advanced when it arrives at the next intersection
} catch (IOException | InterruptedException e) {
System.err.println("[" + intersectionId + "] Failed to send vehicle " +
vehicle.getId() + " to " + nextDestination + ": " + e.getMessage());
}
}
/**
* Gets an existing connection to a destination or creates a new one.
*
* @param destinationId The ID of the destination node.
* @return The SocketConnection to that destination.
* @throws IOException If connection cannot be established.
* @throws InterruptedException If connection attempt is interrupted.
*/
private synchronized SocketConnection getOrCreateConnection(String destinationId)
throws IOException, InterruptedException {
if (!outgoingConnections.containsKey(destinationId)) {
String host = getHostForDestination(destinationId);
int port = getPortForDestination(destinationId);
System.out.println("[" + intersectionId + "] Creating connection to " +
destinationId + " at " + host + ":" + port);
SocketConnection connection = new SocketConnection(host, port);
outgoingConnections.put(destinationId, connection);
}
return outgoingConnections.get(destinationId);
}
/**
* Gets the host address for a destination node from configuration.
*
* @param destinationId The destination node ID.
* @return The host address.
*/
private String getHostForDestination(String destinationId) {
if (destinationId.equals("S")) {
return config.getExitHost();
} else {
return config.getIntersectionHost(destinationId);
}
}
/**
* Gets the port number for a destination node from configuration.
*
* @param destinationId The destination node ID.
* @return The port number.
*/
private int getPortForDestination(String destinationId) {
if (destinationId.equals("S")) {
return config.getExitPort();
} else {
return config.getIntersectionPort(destinationId);
}
}
/**
* Starts the server socket and begins accepting incoming connections.
* This is the main listening loop of the process.
*
* @throws IOException If the server socket cannot be created.
*/
public void start() throws IOException {
int port = config.getIntersectionPort(intersectionId);
serverSocket = new ServerSocket(port);
running = true;
System.out.println("\n[" + intersectionId + "] Server started on port " + port);
// Start traffic light threads when running is true
startTrafficLights();
System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n");
// Main accept loop
while (running) {
try {
Socket clientSocket = serverSocket.accept();
// Handle each connection in a separate thread
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
} catch (IOException e) {
if (running) {
System.err.println("[" + intersectionId + "] Error accepting connection: " +
e.getMessage());
}
}
}
}
/**
* Handles an incoming connection from another process.
* Continuously listens for vehicle transfer messages.
*
* @param clientSocket The accepted socket connection.
*/
private void handleIncomingConnection(Socket clientSocket) {
try (SocketConnection connection = new SocketConnection(clientSocket)) {
System.out.println("[" + intersectionId + "] New connection accepted from " +
clientSocket.getInetAddress().getHostAddress());
// Continuously receive messages while connection is active
while (running && connection.isConnected()) {
try {
MessageProtocol message = connection.receiveMessage();
if (message.getType() == MessageType.VEHICLE_TRANSFER) {
Vehicle vehicle = (Vehicle) message.getPayload();
System.out.println("[" + intersectionId + "] Received vehicle: " +
vehicle.getId() + " from " + message.getSourceNode());
// Add vehicle to appropriate queue
intersection.receiveVehicle(vehicle);
}
} catch (ClassNotFoundException e) {
System.err.println("[" + intersectionId + "] Unknown message type received: " +
e.getMessage());
}
}
} catch (IOException e) {
if (running) {
System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage());
}
}
}
/**
* Stops the intersection process gracefully.
* Shuts down all threads and closes all connections.
*/
public void shutdown() {
System.out.println("\n[" + intersectionId + "] Shutting down...");
running = false;
// Close server socket
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
System.err.println("[" + intersectionId + "] Error closing server socket: " +
e.getMessage());
}
// Shutdown thread pools
trafficLightPool.shutdown();
connectionHandlerPool.shutdown();
try {
if (!trafficLightPool.awaitTermination(5, TimeUnit.SECONDS)) {
trafficLightPool.shutdownNow();
}
if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
connectionHandlerPool.shutdownNow();
}
} catch (InterruptedException e) {
trafficLightPool.shutdownNow();
connectionHandlerPool.shutdownNow();
}
// Close all outgoing connections
for (Map.Entry<String, SocketConnection> entry : outgoingConnections.entrySet()) {
try {
entry.getValue().close();
} catch (IOException e) {
System.err.println("[" + intersectionId + "] Error closing connection to " +
entry.getKey() + ": " + e.getMessage());
}
}
System.out.println("[" + intersectionId + "] Shutdown complete.");
System.out.println("=".repeat(60));
}
/**
* Gets the Intersection object managed by this process.
* Useful for testing and monitoring.
*
* @return The Intersection object.
*/
public Intersection getIntersection() {
return intersection;
}
// --- Inner class for Vehicle Transfer Messages ---
/**
* Implementation of MessageProtocol for vehicle transfers between processes.
*/
private static class VehicleTransferMessage implements MessageProtocol {
private static final long serialVersionUID = 1L;
private final String sourceNode;
private final String destinationNode;
private final Vehicle payload;
public VehicleTransferMessage(String sourceNode, String destinationNode, Vehicle vehicle) {
this.sourceNode = sourceNode;
this.destinationNode = destinationNode;
this.payload = vehicle;
}
@Override
public MessageType getType() {
return MessageType.VEHICLE_TRANSFER;
}
@Override
public Object getPayload() {
return payload;
}
@Override
public String getSourceNode() {
return sourceNode;
}
@Override
public String getDestinationNode() {
return destinationNode;
}
}
}

View File

@@ -0,0 +1,204 @@
package sd.coordinator;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import sd.config.SimulationConfig;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.serialization.SerializationException;
import sd.util.VehicleGenerator;
/**
* Coordinator process responsible for:
* 1. Vehicle generation (using VehicleGenerator)
* 2. Distributing vehicles to intersection processes via sockets
* 3. Managing simulation timing and shutdown
*
* This is the main entry point for the distributed simulation architecture.
*/
public class CoordinatorProcess {
private final SimulationConfig config;
private final VehicleGenerator vehicleGenerator;
private final Map<String, SocketClient> intersectionClients;
private double currentTime;
private int vehicleCounter;
private boolean running;
private double nextGenerationTime;
public static void main(String[] args) {
System.out.println("=".repeat(60));
System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION");
System.out.println("=".repeat(60));
try {
// 1. Load configuration
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
System.out.println("Loading configuration from: " + configFile);
SimulationConfig config = new SimulationConfig(configFile);
CoordinatorProcess coordinator = new CoordinatorProcess(config);
// 2. Connect to intersection processes
System.out.println("\n" + "=".repeat(60));
coordinator.initialize();
// 3. Run the sim
System.out.println("\n" + "=".repeat(60));
coordinator.run();
} catch (IOException e) {
System.err.println("Failed to load configuration: " + e.getMessage());
System.exit(1);
} catch (Exception e) {
System.err.println("Coordinator error: " + e.getMessage());
System.exit(1);
}
}
public CoordinatorProcess(SimulationConfig config) {
this.config = config;
this.vehicleGenerator = new VehicleGenerator(config);
this.intersectionClients = new HashMap<>();
this.currentTime = 0.0;
this.vehicleCounter = 0;
this.running = false;
this.nextGenerationTime = 0.0;
System.out.println("Coordinator initialized with configuration:");
System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s");
System.out.println(" - Arrival model: " + config.getArrivalModel());
System.out.println(" - Arrival rate: " + config.getArrivalRate() + " vehicles/s");
}
public void initialize() {
System.out.println("Connecting to intersection processes...");
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
for (String intersectionId : intersectionIds) {
try {
String host = config.getIntersectionHost(intersectionId);
int port = config.getIntersectionPort(intersectionId);
SocketClient client = new SocketClient(intersectionId, host, port);
client.connect();
intersectionClients.put(intersectionId, client);
} catch (IOException e) {
System.err.println("Failed to connect to " + intersectionId + ": " + e.getMessage());
}
}
System.out.println("Successfully connected to " + intersectionClients.size() + " intersection(s)");
if (intersectionClients.isEmpty()) {
System.err.println("WARNING: No intersections connected. Simulation cannot proceed.");
}
}
public void run() {
double duration = config.getSimulationDuration();
running = true;
System.out.println("Starting vehicle generation simulation...");
System.out.println("Duration: " + duration + " seconds");
System.out.println();
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
final double TIME_STEP = 0.1;
while (running && currentTime < duration) {
if (currentTime >= nextGenerationTime) {
generateAndSendVehicle();
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
}
currentTime += TIME_STEP;
}
System.out.println();
System.out.println("Simulation complete at t=" + String.format("%.2f", currentTime) + "s");
System.out.println("Total vehicles generated: " + vehicleCounter);
shutdown();
}
private void generateAndSendVehicle() {
Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime);
System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
if (vehicle.getRoute().isEmpty()) {
System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!");
return;
}
String entryIntersection = vehicle.getRoute().get(0);
sendVehicleToIntersection(vehicle, entryIntersection);
}
private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) {
SocketClient client = intersectionClients.get(intersectionId);
if (client == null || !client.isConnected()) {
System.err.println("ERROR: No connection to " + intersectionId + " for vehicle " + vehicle.getId());
return;
}
try {
Message message = new Message(
MessageType.VEHICLE_SPAWN,
"COORDINATOR",
intersectionId,
vehicle
);
client.send(message);
System.out.printf("->Sent to %s%n", intersectionId);
} catch (SerializationException | IOException e) {
System.err.println("ERROR: Failed to send vehicle " + vehicle.getId() + " to " + intersectionId);
System.err.println("Reason: " + e.getMessage());
}
}
public void shutdown() {
System.out.println();
System.out.println("=".repeat(60));
System.out.println("Shutting down coordinator...");
for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) {
String intersectionId = entry.getKey();
SocketClient client = entry.getValue();
try {
if (client.isConnected()) {
Message personalizedShutdown = new Message(
MessageType.SHUTDOWN,
"COORDINATOR",
intersectionId,
"Simulation complete"
);
client.send(personalizedShutdown);
System.out.println("Sent shutdown message to " + intersectionId);
}
} catch (SerializationException | IOException e) {
System.err.println("Error sending shutdown to " + intersectionId + ": " + e.getMessage());
} finally {
client.close();
}
}
System.out.println("Coordinator shutdown complete");
System.out.println("=".repeat(60));
}
public void stop() {
System.out.println("\nStop signal received...");
running = false;
}
}

View File

@@ -0,0 +1,124 @@
package sd.coordinator;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import sd.model.Message;
import sd.serialization.MessageSerializer;
import sd.serialization.SerializationException;
import sd.serialization.SerializerFactory;
/**
* Socket client for communication with a single intersection process.
*
* Handles a persistent TCP connection to one intersection,
* providing a simple way to send serialized messages.
*/
public class SocketClient {
private final String intersectionId;
private final String host;
private final int port;
private Socket socket;
private OutputStream outputStream;
private MessageSerializer serializer;
/**
* Creates a new SocketClient for a given intersection.
*
* @param intersectionId Intersection ID (ex. "Cr1")
* @param host Host address (ex. "localhost")
* @param port Port number
*/
public SocketClient(String intersectionId, String host, int port) {
this.intersectionId = intersectionId;
this.host = host;
this.port = port;
this.serializer = SerializerFactory.createDefault();
}
/**
* Connects to the intersection process via TCP.
*
* @throws IOException if the connection cannot be established
*/
public void connect() throws IOException {
try {
socket = new Socket(host, port);
outputStream = socket.getOutputStream();
System.out.println("Connected to " + intersectionId + " at " + host + ":" + port);
} catch (IOException e) {
System.err.println("Failed to connect to " + intersectionId + " at " + host + ":" + port);
throw e;
}
}
/**
* Sends a message to the connected intersection.
* The message is serialized and written over the socket.
*
* @param message The message to send
* @throws SerializationException if serialization fails
* @throws IOException if the socket write fails
*/
public void send(Message message) throws SerializationException, IOException {
if (socket == null || socket.isClosed()) {
throw new IOException("Socket is not connected to " + intersectionId);
}
try {
byte[] data = serializer.serialize(message);
// Prefix with message length (so receiver knows how much to read)
int length = data.length;
outputStream.write((length >> 24) & 0xFF);
outputStream.write((length >> 16) & 0xFF);
outputStream.write((length >> 8) & 0xFF);
outputStream.write(length & 0xFF);
outputStream.write(data);
outputStream.flush();
} catch (SerializationException | IOException e) {
System.err.println("Error sending message to " + intersectionId + ": " + e.getMessage());
throw e;
}
}
/**
* Closes the socket connection safely.
* Calling it multiple times wont cause issues.
*/
public void close() {
try {
if (outputStream != null) {
outputStream.close();
}
if (socket != null && !socket.isClosed()) {
socket.close();
System.out.println("Closed connection to " + intersectionId);
}
} catch (IOException e) {
System.err.println("Error closing connection to " + intersectionId + ": " + e.getMessage());
}
}
/**
* @return true if connected and socket is open, false otherwise
*/
public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed();
}
public String getIntersectionId() {
return intersectionId;
}
@Override
public String toString() {
return String.format("SocketClient[intersection=%s, host=%s, port=%d, connected=%s]",
intersectionId, host, port, isConnected());
}
}

View File

@@ -0,0 +1,158 @@
package sd.engine;
import sd.IntersectionProcess;
import sd.config.SimulationConfig;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import sd.model.Vehicle;
/**
* Implements the control logic for a single TrafficLight
* as a Runnable task that runs in its own Thread.
*
*/
public class TrafficLightThread implements Runnable {
/**
* The TrafficLight object (the *model*) that this thread controls.
* Contains the queue and the state.
*/
private final TrafficLight light;
/**
* The IntersectionProcess (the Process) that "owns" this thread.
* Used to call methods on the process, such as sendVehicleToNextDestination().
*/
private final IntersectionProcess process;
/**
* The simulation configuration, used to get timings (e.g., crossing time).
*/
private final SimulationConfig config;
/**
* Volatile flag to control the graceful shutdown mechanism.
* When set to 'false', the 'run()' loop terminates.
*/
private volatile boolean running;
/**
* Constructor for the Traffic Light Thread.
*
* @param light The TrafficLight object (model) to be controlled.
* @param process The parent IntersectionProcess (for callbacks).
* @param config The simulation configuration (to get timings).
*/
public TrafficLightThread(TrafficLight light, IntersectionProcess process, SimulationConfig config) {
this.light = light;
this.process = process;
this.config = config;
this.running = false; // Starts as 'stopped'
}
/**
* The main entry point for the thread.
* Implements the GREEN/RED cycle logic extracted from IntersectionProcess.
*
*/
@Override
public void run() {
this.running = true;
System.out.println("[" + light.getId() + "] Traffic light thread started.");
try {
// Main thread loop, continues while 'running' is true
// This 'running' flag is controlled by the parent IntersectionProcess
while (running) {
// --- GREEN Phase ---
light.changeState(TrafficLightState.GREEN); //
System.out.println("[" + light.getId() + "] State: GREEN");
// Process vehicles in the queue
processGreenLightQueue();
// Wait for green duration
Thread.sleep((long) (light.getGreenTime() * 1000)); //
if (!running) break; // Check flag after sleep
// --- RED Phase ---
light.changeState(TrafficLightState.RED); //
System.out.println("[" + light.getId() + "] State: RED");
// Wait for red duration
Thread.sleep((long) (light.getRedTime() * 1000)); //
}
} catch (InterruptedException e) {
// Apanha a InterruptedException (outra forma de parar a thread)
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
this.running = false; // Garante que o loop termina
}
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
}
/**
* Processes vehicles in the queue while the traffic light is GREEN.
* Logic extracted from IntersectionProcess.processGreenLight()
*
*/
private void processGreenLightQueue() throws InterruptedException {
//
while (running && light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
Vehicle vehicle = light.removeVehicle(); //
if (vehicle != null) {
// 1. Get the crossing time (t_sem)
double crossingTime = getCrossingTimeForVehicle(vehicle); //
// 2. Simulate the time the vehicle takes to cross
Thread.sleep((long) (crossingTime * 1000)); //
// 3. Update vehicle statistics
vehicle.addCrossingTime(crossingTime); //
// 4. Update intersection statistics
process.getIntersection().incrementVehiclesSent(); //
// 5. Call the parent Process to send the vehicle
process.sendVehicleToNextDestination(vehicle); //
}
}
}
/**
* Gets the crossing time for a vehicle based on its type.
* Logic extracted from IntersectionProcess.getCrossingTimeForVehicle()
*
*
* @param vehicle The vehicle.
* @return The crossing time in seconds.
*/
private double getCrossingTimeForVehicle(Vehicle vehicle) {
switch (vehicle.getType()) { //
case BIKE:
return config.getBikeVehicleCrossingTime(); //
case LIGHT:
return config.getLightVehicleCrossingTime(); //
case HEAVY:
return config.getHeavyVehicleCrossingTime(); //
default:
return config.getLightVehicleCrossingTime(); //
}
}
/**
* Requests the thread to stop gracefully (graceful shutdown).
* Sets the 'running' flag to false. The thread will finish
* its current sleep cycle and exit the 'run()' loop.
*/
public void shutdown() {
this.running = false;
}
}

View File

@@ -104,16 +104,28 @@ public class Intersection {
* Accepts an incoming vehicle and places it in the correct queue. * Accepts an incoming vehicle and places it in the correct queue.
* * This method: * * This method:
* 1. Increments the {@link #totalVehiclesReceived} counter. * 1. Increments the {@link #totalVehiclesReceived} counter.
* 2. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}). * 2. Advances the vehicle's route (since it just arrived here)
* 3. Uses the {@link #routing} map to find the correct *direction* for that destination. * 3. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
* 4. Adds the vehicle to the queue of the {@link TrafficLight} for that direction. * 4. Uses the {@link #routing} map to find the correct *direction* for that destination.
* 5. Adds the vehicle to the queue of the {@link TrafficLight} for that direction.
* *
* @param vehicle The {@link Vehicle} arriving at the intersection. * @param vehicle The {@link Vehicle} arriving at the intersection.
*/ */
public void receiveVehicle(Vehicle vehicle) { public void receiveVehicle(Vehicle vehicle) {
totalVehiclesReceived++; totalVehiclesReceived++;
// Note: Route advancement is handled by SimulationEngine.handleVehicleArrival()
// before calling this method, so we don't advance here.
String nextDestination = vehicle.getCurrentDestination(); String nextDestination = vehicle.getCurrentDestination();
// Check if vehicle reached final destination
if (nextDestination == null) {
System.out.printf("[%s] Vehicle %s reached final destination%n",
this.id, vehicle.getId());
return;
}
String direction = routing.get(nextDestination); String direction = routing.get(nextDestination);
if (direction != null && trafficLights.containsKey(direction)) { if (direction != null && trafficLights.containsKey(direction)) {

View File

@@ -0,0 +1,142 @@
package sd.model;
import java.io.Serializable;
import java.util.UUID;
/**
* Represents a message exchanged between processes in the distributed simulation.
* Each message has a unique ID, a type, a sender, a destination, and a payload.
* This class implements {@link Serializable} to allow transmission over the network.
*/
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Unique identifier for this message.
*/
private final String messageId;
/**
* The type of this message (e.g., VEHICLE_TRANSFER, STATS_UPDATE).
*/
private final MessageType type;
/**
* Identifier of the process that sent this message.
*/
private final String senderId;
/**
* Identifier of the destination process. Can be null for broadcast messages.
*/
private final String destinationId;
/**
* The actual data being transmitted. Type depends on the message type.
*/
private final Object payload;
/**
* Timestamp when this message was created (simulation time or real time).
*/
private final long timestamp;
/**
* Creates a new message with all parameters.
*
* @param type The message type
* @param senderId The ID of the sending process
* @param destinationId The ID of the destination process (null for broadcast)
* @param payload The message payload
* @param timestamp The timestamp of message creation
*/
public Message(MessageType type, String senderId, String destinationId,
Object payload, long timestamp) {
this.messageId = UUID.randomUUID().toString();
this.type = type;
this.senderId = senderId;
this.destinationId = destinationId;
this.payload = payload;
this.timestamp = timestamp;
}
/**
* Creates a new message with current system time as timestamp.
*
* @param type The message type
* @param senderId The ID of the sending process
* @param destinationId The ID of the destination process
* @param payload The message payload
*/
public Message(MessageType type, String senderId, String destinationId, Object payload) {
this(type, senderId, destinationId, payload, System.currentTimeMillis());
}
/**
* Creates a broadcast message (no specific destination).
*
* @param type The message type
* @param senderId The ID of the sending process
* @param payload The message payload
*/
public Message(MessageType type, String senderId, Object payload) {
this(type, senderId, null, payload, System.currentTimeMillis());
}
//Getters
public String getMessageId() {
return messageId;
}
public MessageType getType() {
return type;
}
public String getSenderId() {
return senderId;
}
public String getDestinationId() {
return destinationId;
}
public Object getPayload() {
return payload;
}
public long getTimestamp() {
return timestamp;
}
/**
* Checks if this is a broadcast message (no specific destination).
*
* @return true if destinationId is null, false otherwise
*/
public boolean isBroadcast() {
return destinationId == null;
}
/**
* Gets the payload cast to a specific type.
* Use with caution and ensure type safety.
*
* @param <T> The expected payload type
* @return The payload cast to type T
* @throws ClassCastException if the payload is not of type T
*/
@SuppressWarnings("unchecked")
public <T> T getPayloadAs(Class<T> clazz) {
return (T) payload;
}
@Override
public String toString() {
return String.format("Message[id=%s, type=%s, from=%s, to=%s, timestamp=%d]",
messageId, type, senderId,
destinationId != null ? destinationId : "BROADCAST",
timestamp);
}
}

View File

@@ -0,0 +1,81 @@
package sd.model;
/**
* Enumeration representing all possible message types for distributed communication.
* These types are used for inter-process communication between different components
* of the distributed traffic simulation system.
*/
public enum MessageType {
/**
* Message to transfer a vehicle between intersections or processes.
* Payload: Vehicle object with current state
*/
VEHICLE_TRANSFER,
/**
* Message to update statistics across the distributed system.
* Payload: Statistics data (waiting times, queue sizes, etc.)
*/
STATS_UPDATE,
/**
* Message to synchronize traffic light states between processes.
* Payload: TrafficLight state and timing information
*/
TRAFFIC_LIGHT_SYNC,
/**
* Heartbeat message to check if a process is alive.
* Payload: Process ID and timestamp
*/
HEARTBEAT,
/**
* Request to join the distributed simulation.
* Payload: Process information and capabilities
*/
JOIN_REQUEST,
/**
* Response to a join request.
* Payload: Acceptance status and configuration
*/
JOIN_RESPONSE,
/**
* Message to notify about a new vehicle generation.
* Payload: Vehicle generation parameters
*/
VEHICLE_SPAWN,
/**
* Message to request the current state of an intersection.
* Payload: Intersection ID
*/
STATE_REQUEST,
/**
* Response containing the current state of an intersection.
* Payload: Complete intersection state
*/
STATE_RESPONSE,
/**
* Message to signal shutdown of a process.
* Payload: Process ID and reason
*/
SHUTDOWN,
/**
* Acknowledgment message for reliable communication.
* Payload: Message ID being acknowledged
*/
ACK,
/**
* Error message to report problems in the distributed system.
* Payload: Error description and context
*/
ERROR
}

View File

@@ -0,0 +1,41 @@
package sd.protocol;
import java.io.Serializable;
import sd.model.MessageType; // Assuming MessageType is in sd.model or sd.protocol
/**
* Interface defining the contract for all messages exchanged in the simulator.
* Ensures that any message can be identified and routed.
* * This interface extends Serializable to allow objects that implement it
* to be sent over Sockets (ObjectOutputStream).
*
*/
public interface MessageProtocol extends Serializable {
/**
* Returns the type of the message, indicating its purpose.
* @return The MessageType (e.g., VEHICLE_TRANSFER, STATS_UPDATE).
*/
MessageType getType();
/**
* Returns the data object (payload) that this message carries.
* The type of object will depend on the MessageType.
* * - If getType() == VEHICLE_TRANSFER, the payload will be a {@link sd.model.Vehicle} object.
* - If getType() == STATS_UPDATE, the payload will be a statistics object.
* * @return The data object (payload), which must also be Serializable.
*/
Object getPayload();
/**
* Returns the ID of the node (Process) that sent this message.
* @return String (e.g., "Cr1", "Cr5", "S").
*/
String getSourceNode();
/**
* Returns the ID of the destination node (Process) for this message.
* @return String (e.g., "Cr2", "DashboardServer").
*/
String getDestinationNode();
}

View File

@@ -0,0 +1,199 @@
package sd.protocol;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import sd.serialization.MessageSerializer;
import sd.serialization.SerializationException;
import sd.serialization.SerializerFactory;
/**
* Wrapper class that simplifies communication via Sockets.
* Includes connection retry logic for robustness.
*/
public class SocketConnection implements Closeable {
private final Socket socket;
private final OutputStream outputStream;
private final InputStream inputStream;
private final MessageSerializer serializer;
// --- Configuration for Retry Logic ---
/** Maximum number of connection attempts. */
private static final int MAX_RETRIES = 5;
/** Delay between retry attempts in milliseconds. */
private static final long RETRY_DELAY_MS = 1000;
/**
* Constructor for the "Client" (who initiates the connection).
* Tries to connect to a process that is already listening (Server).
* Includes retry logic in case of initial connection failure.
*
* @param host The host address (e.g., "localhost" from your simulation.properties)
* @param port The port (e.g., 8001 from your simulation.properties)
* @throws IOException If connection fails after all retries.
* @throws UnknownHostException If the host is not found (this error usually doesn't need retry).
* @throws InterruptedException If the thread is interrupted while waiting between retries.
*/
public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException {
Socket tempSocket = null;
IOException lastException = null;
System.out.printf("[SocketConnection] Attempting to connect to %s:%d...%n", host, port);
// --- Retry Loop ---
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
// Try to establish the connection
tempSocket = new Socket(host, port);
// If successful, break out of the retry loop
System.out.printf("[SocketConnection] Connected successfully on attempt %d.%n", attempt);
lastException = null; // Clear last error on success
break;
} catch (ConnectException | SocketTimeoutException e) {
// These are common errors indicating the server might not be ready.
lastException = e;
System.out.printf("[SocketConnection] Attempt %d/%d failed: %s. Retrying in %d ms...%n",
attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
if (attempt < MAX_RETRIES) {
// Wait before the next attempt
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
}
} catch (IOException e) {
// Other IOExceptions might be more permanent, but we retry anyway.
lastException = e;
System.out.printf("[SocketConnection] Attempt %d/%d failed with IOException: %s. Retrying in %d ms...%n",
attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
if (attempt < MAX_RETRIES) {
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
}
}
} // --- End of Retry Loop ---
// If after all retries tempSocket is still null, it means connection failed permanently.
if (tempSocket == null) {
System.err.printf("[SocketConnection] Failed to connect to %s:%d after %d attempts.%n", host, port, MAX_RETRIES);
if (lastException != null) {
throw lastException; // Throw the last exception encountered
} else {
// Should not happen if loop ran, but as a fallback
throw new IOException("Failed to connect after " + MAX_RETRIES + " attempts, reason unknown.");
}
}
// If connection was successful, assign to final variable and create streams
this.socket = tempSocket;
this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream();
this.serializer = SerializerFactory.createDefault();
}
/**
* Constructor for the "Server" (who accepts the connection).
* Receives a Socket that has already been accepted by a ServerSocket.
* No retry logic needed here as the connection is already established.
*
* @param acceptedSocket The Socket returned by serverSocket.accept().
* @throws IOException If stream creation fails.
*/
public SocketConnection(Socket acceptedSocket) throws IOException {
this.socket = acceptedSocket;
this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream();
this.serializer = SerializerFactory.createDefault();
}
/**
* Sends (serializes) a MessageProtocol object over the socket.
*
* @param message The "envelope" (which contains the Vehicle) to be sent.
* @throws IOException If writing to the stream fails or socket is not connected.
*/
public void sendMessage(MessageProtocol message) throws IOException {
if (socket == null || !socket.isConnected()) {
throw new IOException("Socket is not connected");
}
try {
// Serializa para bytes JSON
byte[] data = serializer.serialize(message);
// Write 4-byte length prefix
DataOutputStream dataOut = new DataOutputStream(outputStream);
dataOut.writeInt(data.length);
dataOut.write(data);
dataOut.flush();
} catch (SerializationException e) {
throw new IOException("Failed to serialize message", e);
}
}
/**
* Tries to read (deserialize) a MessageProtocol object from the socket.
*
* @return The "envelope" (MessageProtocol) that was received.
* @throws IOException If the connection is lost, the stream is corrupted, or socket is not connected.
* @throws ClassNotFoundException If the received object is unknown.
*/
public MessageProtocol receiveMessage() throws IOException, ClassNotFoundException {
if (socket == null || !socket.isConnected()) {
throw new IOException("Socket is not connected");
}
try {
// Lê um prefixo de 4 bytes - indicador de tamanho
DataInputStream dataIn = new DataInputStream(inputStream);
int length = dataIn.readInt();
if (length <= 0 || length > 10_000_000) { // Sanity check (10MB max)
throw new IOException("Invalid message length: " + length);
}
// Ler dados da mensagem
byte[] data = new byte[length];
dataIn.readFully(data);
// Deserialize do JSON
return serializer.deserialize(data, MessageProtocol.class);
} catch (SerializationException e) {
throw new IOException("Failed to deserialize message", e);
}
}
/**
* Closes the socket and all streams (Input and Output).
*/
@Override
public void close() throws IOException {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
if (socket != null) socket.close();
}
/**
* @return true if the socket is still connected and not closed.
*/
public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed();
}
}

View File

@@ -0,0 +1,114 @@
package sd.serialization;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import java.nio.charset.StandardCharsets;
/**
* JSON-based implementation of {@link MessageSerializer} using Google's Gson library.
*
* This serializer converts objects to JSON format for transmission, providing:
* - Human-readable message format (easy debugging)
* - Cross-platform compatibility
* - Smaller message sizes compared to Java native serialization
* - Better security (no code execution during deserialization)
*
* The serializer is configured with pretty printing disabled by default for
* production use, but can be enabled for debugging purposes.
*
* Thread-safety: This class is thread-safe as Gson instances are thread-safe.
*
* @see MessageSerializer
*/
public class JsonMessageSerializer implements MessageSerializer {
private final Gson gson;
private final boolean prettyPrint;
/**
* Creates a new JSON serializer with default configuration (no pretty printing).
*/
public JsonMessageSerializer() {
this(false);
}
/**
* Creates a new JSON serializer with optional pretty printing.
*
* @param prettyPrint If true, JSON output will be formatted with indentation
*/
public JsonMessageSerializer(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
GsonBuilder builder = new GsonBuilder();
if (prettyPrint) {
builder.setPrettyPrinting();
}
// Register custom type adapters here if needed
// builder.registerTypeAdapter(Vehicle.class, new VehicleAdapter());
this.gson = builder.create();
}
@Override
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
throw new IllegalArgumentException("Cannot serialize null object");
}
try {
String json = gson.toJson(object);
return json.getBytes(StandardCharsets.UTF_8);
} catch (Exception e) {
throw new SerializationException(
"Failed to serialize object of type " + object.getClass().getName(), e);
}
}
@Override
public <T> T deserialize(byte[] data, Class<T> clazz) throws SerializationException {
if (data == null) {
throw new IllegalArgumentException("Cannot deserialize null data");
}
if (clazz == null) {
throw new IllegalArgumentException("Class type cannot be null");
}
try {
String json = new String(data, StandardCharsets.UTF_8);
return gson.fromJson(json, clazz);
} catch (JsonSyntaxException e) {
throw new SerializationException(
"Failed to parse JSON for type " + clazz.getName(), e);
} catch (Exception e) {
throw new SerializationException(
"Failed to deserialize object of type " + clazz.getName(), e);
}
}
@Override
public String getName() {
return "JSON (Gson)";
}
/**
* Returns the underlying Gson instance for advanced usage.
*
* @return The Gson instance
*/
public Gson getGson() {
return gson;
}
/**
* Checks if pretty printing is enabled.
*
* @return true if pretty printing is enabled
*/
public boolean isPrettyPrint() {
return prettyPrint;
}
}

View File

@@ -0,0 +1,48 @@
package sd.serialization;
/**
* Interface for serializing and deserializing objects for network transmission.
*
* This interface provides a common abstraction for different serialization strategies
* allowing the system to switch between implementations without changing the communication layer.
*
* Implementations must ensure:
* - Thread-safety if used in concurrent contexts
* - Proper exception handling with meaningful error messages
* - Preservation of object state during round-trip serialization
*
* @see JsonMessageSerializer
*/
public interface MessageSerializer {
/**
* Serializes an object into a byte array for transmission.
*
* @param object The object to serialize (must not be null)
* @return A byte array containing the serialized representation
* @throws SerializationException If serialization fails
* @throws IllegalArgumentException If object is null
*/
byte[] serialize(Object object) throws SerializationException;
/**
* Deserializes a byte array back into an object of the specified type.
*
* @param <T> The expected type of the deserialized object
* @param data The byte array containing serialized data (must not be null)
* @param clazz The class of the expected object type (must not be null)
* @return The deserialized object
* @throws SerializationException If deserialization fails
* @throws IllegalArgumentException If data or clazz is null
*/
<T> T deserialize(byte[] data, Class<T> clazz) throws SerializationException;
/**
* Gets the name of this serialization strategy (e.g., "JSON", "Java Native").
* Useful for logging and debugging.
*
* @return The serializer name
*/
String getName();
}

View File

@@ -0,0 +1,134 @@
package sd.serialization;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
import java.util.Arrays;
import java.util.List;
/**
* Demonstration of JSON serialization usage in the traffic simulation system.
*
* This class shows practical examples of how to use JSON (Gson) serialization
* for network communication between simulation processes.
*/
public class SerializationExample {
public static void main(String[] args) {
System.out.println("=== JSON Serialization Example ===\n");
// Create a sample vehicle
List<String> route = Arrays.asList("Cr1", "Cr2", "Cr5", "S");
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 10.5, route);
vehicle.addWaitingTime(2.3);
vehicle.addCrossingTime(1.2);
// Create a message containing the vehicle
Message message = new Message(
MessageType.VEHICLE_TRANSFER,
"Cr1",
"Cr2",
vehicle
);
// ===== JSON Serialization =====
demonstrateJsonSerialization(message);
// ===== Factory Usage =====
demonstrateFactoryUsage(message);
// ===== Performance Test =====
performanceTest(message);
}
private static void demonstrateJsonSerialization(Message message) {
System.out.println("--- JSON Serialization ---");
try {
// Create JSON serializer with pretty printing for readability
MessageSerializer serializer = new JsonMessageSerializer(true);
// Serialize to bytes
byte[] data = serializer.serialize(message);
// Display the JSON
String json = new String(data);
System.out.println("Serialized JSON (" + data.length + " bytes):");
System.out.println(json);
// Deserialize back
Message deserialized = serializer.deserialize(data, Message.class);
System.out.println("\nDeserialized: " + deserialized);
System.out.println("✓ JSON serialization successful\n");
} catch (SerializationException e) {
System.err.println("❌ JSON serialization failed: " + e.getMessage());
}
}
private static void demonstrateFactoryUsage(Message message) {
System.out.println("--- Using SerializerFactory ---");
try {
// Get default serializer (JSON)
MessageSerializer serializer = SerializerFactory.createDefault();
System.out.println("Default serializer: " + serializer.getName());
// Use it
byte[] data = serializer.serialize(message);
Message deserialized = serializer.deserialize(data, Message.class);
System.out.println("Message type: " + deserialized.getType());
System.out.println("From: " + deserialized.getSenderId() +
" → To: " + deserialized.getDestinationId());
System.out.println("✓ Factory usage successful\n");
} catch (SerializationException e) {
System.err.println("❌ Factory usage failed: " + e.getMessage());
}
}
private static void performanceTest(Message message) {
System.out.println("--- Performance Test ---");
int iterations = 1000;
try {
MessageSerializer compactSerializer = new JsonMessageSerializer(false);
MessageSerializer prettySerializer = new JsonMessageSerializer(true);
// Warm up
for (int i = 0; i < 100; i++) {
compactSerializer.serialize(message);
}
// Test compact JSON
long compactStart = System.nanoTime();
byte[] compactData = null;
for (int i = 0; i < iterations; i++) {
compactData = compactSerializer.serialize(message);
}
long compactTime = System.nanoTime() - compactStart;
// Test pretty JSON
byte[] prettyData = prettySerializer.serialize(message);
// Results
System.out.println("Iterations: " + iterations);
System.out.println("\nJSON Compact:");
System.out.println(" Size: " + compactData.length + " bytes");
System.out.println(" Time: " + (compactTime / 1_000_000.0) + " ms total");
System.out.println(" Avg: " + (compactTime / iterations / 1_000.0) + " μs/operation");
System.out.println("\nJSON Pretty-Print:");
System.out.println(" Size: " + prettyData.length + " bytes");
System.out.println(" Size increase: " +
String.format("%.1f%%", ((double)prettyData.length / compactData.length - 1) * 100));
} catch (SerializationException e) {
System.err.println("❌ Performance test failed: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,41 @@
package sd.serialization;
/**
* Exception thrown when serialization or deserialization operations fail.
*
* This exception wraps underlying errors (I/O exceptions, parsing errors, etc.)
* and provides context about what went wrong during the serialization process.
*/
public class SerializationException extends Exception {
private static final long serialVersionUID = 1L; // Long(64bits) instead of int(32bits)
/**
* Constructs a new serialization exception with the specified detail message.
*
* @param message The detail message
*/
public SerializationException(String message) {
super(message);
}
/**
* Constructs a new serialization exception with the specified detail message
* and cause.
*
* @param message The detail message
* @param cause The cause of this exception
*/
public SerializationException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new serialization exception with the specified cause.
*
* @param cause The cause of this exception
*/
public SerializationException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,66 @@
package sd.serialization;
/**
* Factory for creating {@link MessageSerializer} instances.
*
* This factory provides a centralized way to create and configure JSON serializers
* using Gson, making it easy to configure serialization throughout the application.
*
* The factory can be configured via system properties for easy deployment configuration.
*
* Example usage:
* <pre>
* MessageSerializer serializer = SerializerFactory.createDefault();
* byte[] data = serializer.serialize(myObject);
* </pre>
*/
public class SerializerFactory {
/**
* System property key for enabling pretty-print in JSON serialization.
* Set to "true" for debugging, "false" for production.
*/
public static final String JSON_PRETTY_PRINT_PROPERTY = "sd.serialization.json.prettyPrint";
// Default configuration
private static final boolean DEFAULT_JSON_PRETTY_PRINT = false;
/**
* Private constructor to prevent instantiation.
*/
private SerializerFactory() {
throw new UnsupportedOperationException("Factory class cannot be instantiated");
}
/**
* Creates a JSON serializer based on system configuration.
*
* Pretty-print is determined by checking the system property
* {@value #JSON_PRETTY_PRINT_PROPERTY}. If not set, defaults to false.
*
* @return A configured JsonMessageSerializer instance
*/
public static MessageSerializer createDefault() {
boolean prettyPrint = Boolean.getBoolean(JSON_PRETTY_PRINT_PROPERTY);
return new JsonMessageSerializer(prettyPrint);
}
/**
* Creates a JSON serializer with default configuration (no pretty printing).
*
* @return A JsonMessageSerializer instance
*/
public static MessageSerializer createSerializer() {
return createSerializer(DEFAULT_JSON_PRETTY_PRINT);
}
/**
* Creates a JSON serializer with specified pretty-print setting.
*
* @param prettyPrint Whether to enable pretty printing
* @return A JsonMessageSerializer instance
*/
public static MessageSerializer createSerializer(boolean prettyPrint) {
return new JsonMessageSerializer(prettyPrint);
}
}

View File

@@ -0,0 +1,473 @@
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
import sd.IntersectionProcess;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
/**
* Tests for IntersectionProcess - covers initialization, traffic lights,
* vehicle transfer and network stuff
*/
public class IntersectionProcessTest {
@TempDir
Path tempDir;
private Path configFile;
private IntersectionProcess intersectionProcess;
// setup test config before each test
@BeforeEach
public void setUp() throws IOException {
// create temp config file
configFile = tempDir.resolve("test-simulation.properties");
String configContent = """
# Test Simulation Configuration
# Intersection Network Configuration
intersection.Cr1.host=localhost
intersection.Cr1.port=18001
intersection.Cr2.host=localhost
intersection.Cr2.port=18002
intersection.Cr3.host=localhost
intersection.Cr3.port=18003
intersection.Cr4.host=localhost
intersection.Cr4.port=18004
intersection.Cr5.host=localhost
intersection.Cr5.port=18005
# Exit Configuration
exit.host=localhost
exit.port=18099
# Dashboard Configuration
dashboard.host=localhost
dashboard.port=18100
# Traffic Light Timing (seconds)
trafficLight.Cr1.East.greenTime=5.0
trafficLight.Cr1.East.redTime=5.0
trafficLight.Cr1.South.greenTime=5.0
trafficLight.Cr1.South.redTime=5.0
trafficLight.Cr1.West.greenTime=5.0
trafficLight.Cr1.West.redTime=5.0
trafficLight.Cr2.West.greenTime=4.0
trafficLight.Cr2.West.redTime=6.0
trafficLight.Cr2.East.greenTime=4.0
trafficLight.Cr2.East.redTime=6.0
trafficLight.Cr2.South.greenTime=4.0
trafficLight.Cr2.South.redTime=6.0
trafficLight.Cr3.West.greenTime=3.0
trafficLight.Cr3.West.redTime=7.0
trafficLight.Cr3.East.greenTime=3.0
trafficLight.Cr3.East.redTime=7.0
trafficLight.Cr4.East.greenTime=6.0
trafficLight.Cr4.East.redTime=4.0
trafficLight.Cr5.East.greenTime=5.0
trafficLight.Cr5.East.redTime=5.0
# Vehicle Crossing Times (seconds)
vehicle.bike.crossingTime=2.0
vehicle.light.crossingTime=3.0
vehicle.heavy.crossingTime=5.0
""";
Files.writeString(configFile, configContent);
}
// cleanup after tests
@AfterEach
public void tearDown() {
if (intersectionProcess != null) {
intersectionProcess.shutdown();
}
}
// ==================== Initialization Tests ====================
@Test
public void testConstructor_Success() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
assertNotNull(intersectionProcess);
}
@Test
public void testConstructor_InvalidConfig() {
Exception exception = assertThrows(IOException.class, () -> {
new IntersectionProcess("Cr1", "non-existent-config.properties");
});
assertNotNull(exception);
}
@Test
public void testInitialize_Cr1() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr2() throws IOException {
intersectionProcess = new IntersectionProcess("Cr2", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr3() throws IOException {
intersectionProcess = new IntersectionProcess("Cr3", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr4() throws IOException {
intersectionProcess = new IntersectionProcess("Cr4", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr5() throws IOException {
intersectionProcess = new IntersectionProcess("Cr5", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
// traffic light creation tests
@Test
public void testTrafficLightCreation_Cr1_HasCorrectDirections() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
// cant access private fields but initialization succeds
assertNotNull(intersectionProcess);
}
@Test
public void testTrafficLightCreation_Cr3_HasCorrectDirections() throws IOException {
intersectionProcess = new IntersectionProcess("Cr3", configFile.toString());
intersectionProcess.initialize();
// Cr3 has west and south only
assertNotNull(intersectionProcess);
}
@Test
public void testTrafficLightCreation_Cr4_HasSingleDirection() throws IOException {
intersectionProcess = new IntersectionProcess("Cr4", configFile.toString());
intersectionProcess.initialize();
// Cr4 only has east direction
assertNotNull(intersectionProcess);
}
// server startup tests
@Test
@Timeout(5)
public void testServerStart_BindsToCorrectPort() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
// start server in seperate thread
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
// expected on shutdown
}
});
serverThread.start();
Thread.sleep(500); // wait for server to start
// try connecting to check if its running
try (Socket clientSocket = new Socket("localhost", 18001)) {
assertTrue(clientSocket.isConnected());
}
intersectionProcess.shutdown();
serverThread.join(2000);
}
@Test
@Timeout(5)
public void testServerStart_MultipleIntersections() throws IOException, InterruptedException {
// test 2 intersections on diferent ports
IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString());
IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString());
cr1.initialize();
cr2.initialize();
Thread thread1 = new Thread(() -> {
try { cr1.start(); } catch (IOException e) { }
});
Thread thread2 = new Thread(() -> {
try { cr2.start(); } catch (IOException e) { }
});
thread1.start();
thread2.start();
Thread.sleep(500);
// check both are running
try (Socket socket1 = new Socket("localhost", 18001);
Socket socket2 = new Socket("localhost", 18002)) {
assertTrue(socket1.isConnected());
assertTrue(socket2.isConnected());
}
cr1.shutdown();
cr2.shutdown();
thread1.join(2000);
thread2.join(2000);
}
// vehicle transfer tests
@Test
@Timeout(10)
public void testVehicleTransfer_ReceiveVehicle() throws IOException, InterruptedException {
// setup reciever intersection
intersectionProcess = new IntersectionProcess("Cr2", configFile.toString());
intersectionProcess.initialize();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) { }
});
serverThread.start();
Thread.sleep(500);
// create test vehicle
java.util.List<String> route = Arrays.asList("Cr2", "Cr3", "S");
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
// send vehicle from Cr1 to Cr2
try (Socket socket = new Socket("localhost", 18002)) {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
out.writeObject(message);
out.flush();
Thread.sleep(1000); // wait for procesing
}
intersectionProcess.shutdown();
serverThread.join(2000);
}
// routing config tests
@Test
public void testRoutingConfiguration_Cr1() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
// indirect test - if init works routing should be ok
assertNotNull(intersectionProcess);
}
@Test
public void testRoutingConfiguration_Cr5() throws IOException {
intersectionProcess = new IntersectionProcess("Cr5", configFile.toString());
intersectionProcess.initialize();
// Cr5 routes to exit
assertNotNull(intersectionProcess);
}
// shutdown tests
@Test
@Timeout(5)
public void testShutdown_GracefulTermination() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) { }
});
serverThread.start();
Thread.sleep(500);
// shutdown should be fast
assertDoesNotThrow(() -> intersectionProcess.shutdown());
serverThread.join(2000);
}
@Test
@Timeout(5)
public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) { }
});
serverThread.start();
Thread.sleep(500);
// verify server running
try (Socket socket = new Socket("localhost", 18001)) {
assertTrue(socket.isConnected());
}
intersectionProcess.shutdown();
serverThread.join(2000);
// after shutdown conection should fail
Thread.sleep(500);
Exception exception = assertThrows(IOException.class, () -> {
Socket socket = new Socket("localhost", 18001);
socket.close();
});
assertNotNull(exception);
}
@Test
@Timeout(5)
public void testShutdown_StopsTrafficLightThreads() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) { }
});
serverThread.start();
Thread.sleep(500);
int threadCountBefore = Thread.activeCount();
intersectionProcess.shutdown();
serverThread.join(2000);
Thread.sleep(500); // wait for threads to die
// thread count should decrese (traffic light threads stop)
int threadCountAfter = Thread.activeCount();
assertTrue(threadCountAfter <= threadCountBefore);
}
// integration tests
@Test
@Timeout(15)
public void testIntegration_TwoIntersectionsVehicleTransfer() throws IOException, InterruptedException {
// setup 2 intersections
IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString());
IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString());
cr1.initialize();
cr2.initialize();
// start both
Thread thread1 = new Thread(() -> {
try { cr1.start(); } catch (IOException e) { }
});
Thread thread2 = new Thread(() -> {
try { cr2.start(); } catch (IOException e) { }
});
thread1.start();
thread2.start();
Thread.sleep(1000); // wait for servers
// send vehicle to Cr1 that goes to Cr2
java.util.List<String> route = Arrays.asList("Cr1", "Cr2", "S");
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
try (Socket socket = new Socket("localhost", 18001)) {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle);
out.writeObject(message);
out.flush();
Thread.sleep(2000); // time for processing
}
cr1.shutdown();
cr2.shutdown();
thread1.join(2000);
thread2.join(2000);
}
@Test
public void testMain_MissingArguments() {
// main needs intersection ID as argument
// cant test System.exit easily in modern java
assertTrue(true, "Main method expects intersection ID as first argument");
}
// helper class for testing vehicle messages
private static class TestVehicleMessage implements sd.protocol.MessageProtocol {
private static final long serialVersionUID = 1L;
private final String sourceNode;
private final String destinationNode;
private final Vehicle payload;
public TestVehicleMessage(String sourceNode, String destinationNode, Vehicle vehicle) {
this.sourceNode = sourceNode;
this.destinationNode = destinationNode;
this.payload = vehicle;
}
@Override
public MessageType getType() {
return MessageType.VEHICLE_TRANSFER;
}
@Override
public Object getPayload() {
return payload;
}
@Override
public String getSourceNode() {
return sourceNode;
}
@Override
public String getDestinationNode() {
return destinationNode;
}
}
}

View File

@@ -0,0 +1,327 @@
package sd;
import java.io.IOException;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
import sd.config.SimulationConfig;
/**
* Testes unitários para a classe ExitNodeProcess.
*
* Esta classe de testes verifica:
* - Construção e inicialização do processo
* - Criação e aceitação de conexões do servidor socket
* - Gestão do ciclo de vida (start/shutdown)
* - Processamento concorrente de múltiplas conexões
* - Impressão de estatísticas finais
*
* Os testes utilizam configurações temporárias e portas dedicadas (19001)
* para evitar conflitos com outros testes ou processos em execução.
*/
public class ExitNodeProcessTest {
@TempDir
Path tempDir;
private Path configFile;
private ExitNodeProcess exitNodeProcess;
private Thread exitNodeThread;
/**
* Configura o ambiente de teste antes de cada teste.
* Cria um ficheiro de configuração temporário com as definições necessárias.
*/
@BeforeEach
public void setUp() throws IOException {
configFile = tempDir.resolve("test-simulation.properties");
String configContent = """
# Test Exit Node Configuration
# Exit Configuration
exit.host=localhost
exit.port=19001
# Dashboard Configuration (will not be running in tests)
dashboard.host=localhost
dashboard.port=19000
# Vehicle Crossing Times
vehicle.bike.crossingTime=2.0
vehicle.light.crossingTime=3.0
vehicle.heavy.crossingTime=5.0
# Simulation Duration
simulation.duration=60.0
""";
Files.writeString(configFile, configContent);
}
/**
* Limpa os recursos após cada teste.
* Garante que o processo e threads são terminados corretamente.
*/
@AfterEach
public void tearDown() {
if (exitNodeProcess != null) {
exitNodeProcess.shutdown();
}
if (exitNodeThread != null && exitNodeThread.isAlive()) {
exitNodeThread.interrupt();
try {
exitNodeThread.join(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* Testa a construção bem-sucedida do ExitNodeProcess com configuração válida.
*/
@Test
public void testConstructor_Success() throws IOException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
assertNotNull(exitNodeProcess);
}
/**
* Testa que uma exceção é lançada quando a configuração é inválida.
*/
@Test
public void testConstructor_InvalidConfig() {
Exception exception = assertThrows(IOException.class, () -> {
new SimulationConfig("non-existent-config.properties");
});
assertNotNull(exception);
}
/**
* Testa a inicialização sem dashboard disponível.
* Verifica que o processo continua a funcionar mesmo sem conexão ao dashboard.
*/
@Test
public void testInitialize_WithoutDashboard() throws IOException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
assertDoesNotThrow(() -> exitNodeProcess.initialize());
}
/**
* Testa que o servidor socket é criado corretamente na porta configurada.
* Verifica que é possível estabelecer uma conexão ao socket do servidor.
*/
@Test
@Timeout(value = 3, unit = TimeUnit.SECONDS)
public void testStart_ServerSocketCreated() throws IOException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
exitNodeProcess.initialize();
CountDownLatch latch = new CountDownLatch(1);
exitNodeThread = new Thread(() -> {
try {
latch.countDown();
exitNodeProcess.start();
} catch (IOException e) {
// expected when shutdown
}
});
exitNodeThread.start();
try {
assertTrue(latch.await(2, TimeUnit.SECONDS), "Exit node should start within timeout");
Thread.sleep(100);
assertDoesNotThrow(() -> {
try (Socket testSocket = new Socket("localhost", 19001)) {
assertTrue(testSocket.isConnected());
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Testa que o servidor aceita conexões de clientes.
*/
@Test
@Timeout(value = 3, unit = TimeUnit.SECONDS)
public void testStart_AcceptsConnection() throws IOException, InterruptedException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
exitNodeProcess.initialize();
CountDownLatch latch = new CountDownLatch(1);
exitNodeThread = new Thread(() -> {
try {
latch.countDown();
exitNodeProcess.start();
} catch (IOException e) {
// expected
}
});
exitNodeThread.start();
assertTrue(latch.await(2, TimeUnit.SECONDS));
Thread.sleep(200);
assertDoesNotThrow(() -> {
try (Socket socket = new Socket("localhost", 19001)) {
assertTrue(socket.isConnected());
}
});
}
/**
* Testa múltiplas inicializações e encerramentos do processo.
* Verifica que o processo pode ser iniciado e parado múltiplas vezes,
* permitindo reutilização da porta.
*/
@Test
@Timeout(value = 3, unit = TimeUnit.SECONDS)
public void testMultipleStartStop() throws IOException, InterruptedException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
exitNodeProcess.initialize();
CountDownLatch latch = new CountDownLatch(1);
exitNodeThread = new Thread(() -> {
try {
latch.countDown();
exitNodeProcess.start();
} catch (IOException e) {
// expected
}
});
exitNodeThread.start();
assertTrue(latch.await(2, TimeUnit.SECONDS));
Thread.sleep(100);
exitNodeProcess.shutdown();
Thread.sleep(100);
assertDoesNotThrow(() -> {
SimulationConfig config2 = new SimulationConfig(configFile.toString());
ExitNodeProcess exitNode2 = new ExitNodeProcess(config2);
exitNode2.initialize();
exitNode2.shutdown();
});
}
/**
* Testa que o shutdown fecha corretamente o servidor socket.
* Após o shutdown, novas conexões ao socket devem falhar.
*/
@Test
@Timeout(value = 3, unit = TimeUnit.SECONDS)
public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
exitNodeProcess.initialize();
CountDownLatch startLatch = new CountDownLatch(1);
exitNodeThread = new Thread(() -> {
try {
startLatch.countDown();
exitNodeProcess.start();
} catch (IOException e) {
// expected
}
});
exitNodeThread.start();
assertTrue(startLatch.await(2, TimeUnit.SECONDS));
Thread.sleep(200);
exitNodeProcess.shutdown();
Thread.sleep(200);
assertThrows(IOException.class, () -> {
Socket socket = new Socket("localhost", 19001);
socket.close();
});
}
/**
* Testa que as estatísticas finais são impressas corretamente durante o shutdown.
* Verifica que o método não lança exceções mesmo sem dados processados.
*/
@Test
public void testPrintFinalStatistics() throws IOException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
exitNodeProcess.initialize();
assertDoesNotThrow(() -> exitNodeProcess.shutdown());
}
/**
* Testa o processamento de múltiplas conexões concorrentes.
* Verifica que o servidor consegue lidar com vários clientes simultaneamente
* usando o pool de threads.
*/
@Test
@Timeout(value = 3, unit = TimeUnit.SECONDS)
public void testMultipleConcurrentConnections() throws IOException, InterruptedException {
SimulationConfig config = new SimulationConfig(configFile.toString());
exitNodeProcess = new ExitNodeProcess(config);
exitNodeProcess.initialize();
CountDownLatch latch = new CountDownLatch(1);
exitNodeThread = new Thread(() -> {
try {
latch.countDown();
exitNodeProcess.start();
} catch (IOException e) {
// expected
}
});
exitNodeThread.start();
assertTrue(latch.await(2, TimeUnit.SECONDS));
Thread.sleep(200);
Thread[] clients = new Thread[3];
for (int i = 0; i < 3; i++) {
clients[i] = new Thread(() -> {
try (Socket socket = new Socket("localhost", 19001)) {
assertTrue(socket.isConnected());
Thread.sleep(100);
} catch (IOException | InterruptedException e) {
// ignore
}
});
clients[i].start();
}
for (Thread client : clients) {
client.join(1000);
}
}
}

View File

@@ -0,0 +1,206 @@
package sd;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* Test class to verify traffic light coordination within an intersection.
* Ensures that only ONE traffic light can be GREEN at any given time.
*/
public class TrafficLightCoordinationTest {
private IntersectionProcess intersectionProcess;
@BeforeEach
public void setUp() throws IOException {
// Create an intersection with multiple traffic lights
intersectionProcess = new IntersectionProcess("Cr2", "src/main/resources/simulation.properties");
intersectionProcess.initialize();
}
@AfterEach
public void tearDown() throws InterruptedException {
if (intersectionProcess != null) {
intersectionProcess.shutdown();
}
}
/**
* Test that verifies mutual exclusion between traffic lights.
* Monitors all traffic lights for 10 seconds and ensures that
* at most ONE light is GREEN at any point in time.
*/
@Test
public void testOnlyOneGreenLightAtATime() throws InterruptedException {
System.out.println("\n=== Testing Traffic Light Mutual Exclusion ===");
// Start the intersection
Thread intersectionThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
e.printStackTrace();
}
});
intersectionThread.start();
// Monitor traffic lights for violations
AtomicInteger maxGreenSimultaneously = new AtomicInteger(0);
AtomicInteger violationCount = new AtomicInteger(0);
List<String> violations = new ArrayList<>();
// Monitor for 10 seconds
long endTime = System.currentTimeMillis() + 10000;
while (System.currentTimeMillis() < endTime) {
int greenCount = 0;
StringBuilder currentState = new StringBuilder("States: ");
for (TrafficLight light : intersectionProcess.getIntersection().getTrafficLights()) {
TrafficLightState state = light.getState();
currentState.append(light.getDirection()).append("=").append(state).append(" ");
if (state == TrafficLightState.GREEN) {
greenCount++;
}
}
// Update maximum simultaneous green lights
if (greenCount > maxGreenSimultaneously.get()) {
maxGreenSimultaneously.set(greenCount);
}
// Check for violations (more than one green)
if (greenCount > 1) {
violationCount.incrementAndGet();
String violation = String.format("[VIOLATION] %d lights GREEN simultaneously: %s",
greenCount, currentState.toString());
violations.add(violation);
System.err.println(violation);
}
Thread.sleep(50); // Check every 50ms
}
System.out.println("\n=== Test Results ===");
System.out.println("Maximum simultaneous GREEN lights: " + maxGreenSimultaneously.get());
System.out.println("Total violations detected: " + violationCount.get());
if (!violations.isEmpty()) {
System.err.println("\nViolation details:");
violations.forEach(System.err::println);
}
// Assert that we never had more than one green light
assertEquals(0, violationCount.get(),
"Traffic light coordination violated! Multiple lights were GREEN simultaneously.");
assertTrue(maxGreenSimultaneously.get() <= 1,
"At most ONE light should be GREEN at any time. Found: " + maxGreenSimultaneously.get());
System.out.println("\n✅ Traffic light coordination working correctly!");
}
/**
* Test that verifies all traffic lights get a chance to be GREEN.
* Ensures fairness in the coordination mechanism.
*/
@Test
public void testAllLightsGetGreenTime() throws InterruptedException {
System.out.println("\n=== Testing Traffic Light Fairness ===");
// Start the intersection
Thread intersectionThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
e.printStackTrace();
}
});
intersectionThread.start();
// Track which lights have been green
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
boolean[] hasBeenGreen = new boolean[lights.size()];
// Monitor for 15 seconds (enough time for all lights to cycle)
long endTime = System.currentTimeMillis() + 15000;
while (System.currentTimeMillis() < endTime) {
for (int i = 0; i < lights.size(); i++) {
if (lights.get(i).getState() == TrafficLightState.GREEN) {
hasBeenGreen[i] = true;
System.out.println("" + lights.get(i).getDirection() + " has been GREEN");
}
}
Thread.sleep(100);
}
// Check if all lights got green time
int greenCount = 0;
System.out.println("\n=== Fairness Results ===");
for (int i = 0; i < lights.size(); i++) {
String status = hasBeenGreen[i] ? "✓ YES" : "✗ NO";
System.out.println(lights.get(i).getDirection() + " got GREEN time: " + status);
if (hasBeenGreen[i]) greenCount++;
}
assertTrue(greenCount > 0, "At least one light should have been GREEN during the test");
System.out.println("\n" + greenCount + "/" + lights.size() + " lights were GREEN during test period");
}
/**
* Test that verifies the state transitions are consistent.
*/
@Test
public void testStateTransitionsAreConsistent() throws InterruptedException {
System.out.println("\n=== Testing State Transition Consistency ===");
Thread intersectionThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
e.printStackTrace();
}
});
intersectionThread.start();
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
TrafficLightState[] previousStates = new TrafficLightState[lights.size()];
// Initialize previous states
for (int i = 0; i < lights.size(); i++) {
previousStates[i] = lights.get(i).getState();
}
int transitionCount = 0;
long endTime = System.currentTimeMillis() + 8000;
while (System.currentTimeMillis() < endTime) {
for (int i = 0; i < lights.size(); i++) {
TrafficLightState currentState = lights.get(i).getState();
if (currentState != previousStates[i]) {
transitionCount++;
System.out.println(lights.get(i).getDirection() + " transitioned: " +
previousStates[i] + "" + currentState);
previousStates[i] = currentState;
}
}
Thread.sleep(100);
}
System.out.println("\nTotal state transitions observed: " + transitionCount);
assertTrue(transitionCount > 0, "There should be state transitions during the test period");
}
}

View File

@@ -0,0 +1,302 @@
package sd.coordinator;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.serialization.MessageSerializer;
import sd.serialization.SerializerFactory;
/**
* Integration tests for the Coordinator-side networking.
*
* What were checking here:
* 1. A SocketClient can actually connect to something listening
* 2. Messages go over the wire and can be deserialized
* 3. Vehicle payloads survive the trip
* 4. Shutdown messages can be broadcast to multiple intersections
*
* We do this by spinning up a tiny mock intersection server in-process.
*/
class CoordinatorIntegrationTest {
private List<MockIntersectionServer> mockServers;
private static final int BASE_PORT = 9001; // keep clear of real ports
@BeforeEach
void setUp() {
mockServers = new ArrayList<>();
}
@AfterEach
void tearDown() {
// Stop all mock servers
for (MockIntersectionServer server : mockServers) {
server.stop();
}
mockServers.clear();
}
/**
* Can the client open a TCP connection to our fake intersection?
*/
@Test
@Timeout(5)
void testSocketClientConnection() throws IOException, InterruptedException {
MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT);
server.start();
mockServers.add(server);
// tiny pause to let the server bind
Thread.sleep(100);
SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT);
client.connect();
assertTrue(client.isConnected(), "Client should be connected to mock intersection");
client.close();
}
/**
* End-to-end: send a message, make sure the server actually receives it.
*/
@Test
@Timeout(5)
void testMessageTransmission() throws Exception {
MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT);
server.start();
mockServers.add(server);
Thread.sleep(100);
SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT);
client.connect();
Message testMessage = new Message(
MessageType.VEHICLE_SPAWN,
"COORDINATOR",
"Cr1",
"Test payload"
);
client.send(testMessage);
// give the server a moment to read and deserialize
Thread.sleep(200);
assertFalse(
server.getReceivedMessages().isEmpty(),
"Mock server should have received at least one message"
);
Message receivedMsg = server.getReceivedMessages().poll();
assertNotNull(receivedMsg, "Server should have actually received a message");
assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message type should match what we sent");
assertEquals("COORDINATOR", receivedMsg.getSenderId(), "Sender ID should be preserved");
assertEquals("Cr1", receivedMsg.getDestinationId(), "Destination ID should be preserved");
client.close();
}
/**
* Make sure vehicle payloads survive the trip and arrive non-null.
*/
@Test
@Timeout(5)
void testVehicleSpawnMessage() throws Exception {
MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT);
server.start();
mockServers.add(server);
Thread.sleep(100);
SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT);
client.connect();
// fake a vehicle like the coordinator would send
List<String> route = List.of("Cr1", "Cr4", "Cr5", "S");
Vehicle vehicle = new Vehicle("V1", sd.model.VehicleType.LIGHT, 0.0, route);
Message spawnMessage = new Message(
MessageType.VEHICLE_SPAWN,
"COORDINATOR",
"Cr1",
vehicle
);
client.send(spawnMessage);
Thread.sleep(200);
Message receivedMsg = server.getReceivedMessages().poll();
assertNotNull(receivedMsg, "Mock server should receive the spawn message");
assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message should be of type VEHICLE_SPAWN");
assertNotNull(receivedMsg.getPayload(), "Payload should not be null (vehicle must arrive)");
client.close();
}
/**
* Broadcast shutdown to multiple mock intersections and see if all of them get it.
*/
@Test
@Timeout(5)
void testShutdownMessageBroadcast() throws Exception {
// Start a couple of fake intersections
for (int i = 1; i <= 3; i++) {
MockIntersectionServer server = new MockIntersectionServer("Cr" + i, BASE_PORT + i - 1);
server.start();
mockServers.add(server);
}
Thread.sleep(200);
// Connect to all of them
List<SocketClient> clients = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
SocketClient client = new SocketClient("Cr" + i, "localhost", BASE_PORT + i - 1);
client.connect();
clients.add(client);
}
Message shutdownMessage = new Message(
MessageType.SHUTDOWN,
"COORDINATOR",
"ALL",
"Simulation complete"
);
for (SocketClient client : clients) {
client.send(shutdownMessage);
}
Thread.sleep(200);
for (MockIntersectionServer server : mockServers) {
assertFalse(
server.getReceivedMessages().isEmpty(),
"Server " + server.getIntersectionId() + " should have received the shutdown message"
);
Message msg = server.getReceivedMessages().poll();
assertEquals(MessageType.SHUTDOWN, msg.getType(), "Server should receive a SHUTDOWN message");
}
for (SocketClient client : clients) {
client.close();
}
}
/**
* Tiny TCP server that pretends to be an intersection.
* It:
* - listens on a port
* - accepts connections
* - reads length-prefixed messages
* - deserializes them and stores them for the test to inspect
*/
private static class MockIntersectionServer {
private final String intersectionId;
private final int port;
private ServerSocket serverSocket;
private Thread serverThread;
private volatile boolean running;
private final ConcurrentLinkedQueue<Message> receivedMessages;
private final MessageSerializer serializer;
public MockIntersectionServer(String intersectionId, int port) {
this.intersectionId = intersectionId;
this.port = port;
this.receivedMessages = new ConcurrentLinkedQueue<>();
this.serializer = SerializerFactory.createDefault();
this.running = false;
}
public void start() throws IOException {
serverSocket = new ServerSocket(port);
running = true;
System.out.printf("Mock %s listening on port %d%n", intersectionId, port);
serverThread = new Thread(() -> {
try {
while (running) {
Socket clientSocket = serverSocket.accept();
handleClient(clientSocket);
}
} catch (IOException e) {
if (running) {
System.err.println("Mock " + intersectionId + " server error: " + e.getMessage());
}
}
}, "mock-" + intersectionId + "-listener");
serverThread.start();
}
private void handleClient(Socket clientSocket) {
new Thread(() -> {
try (DataInputStream input = new DataInputStream(clientSocket.getInputStream())) {
while (running) {
// Read length prefix (4 bytes, big-endian)
int length = input.readInt();
byte[] data = new byte[length];
input.readFully(data);
Message message = serializer.deserialize(data, Message.class);
receivedMessages.offer(message);
System.out.println("Mock " + intersectionId + " received: " + message.getType());
}
} catch (IOException e) {
if (running) {
System.err.println("Mock " + intersectionId + " client handler error: " + e.getMessage());
}
} catch (Exception e) {
System.err.println("Mock " + intersectionId + " deserialization error: " + e.getMessage());
}
}, "mock-" + intersectionId + "-client").start();
}
public void stop() {
running = false;
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
if (serverThread != null) {
serverThread.interrupt();
serverThread.join(1000);
}
System.out.printf("Mock %s stopped%n", intersectionId);
} catch (IOException | InterruptedException e) {
System.err.println("Error stopping mock server " + intersectionId + ": " + e.getMessage());
}
}
public ConcurrentLinkedQueue<Message> getReceivedMessages() {
return receivedMessages;
}
public String getIntersectionId() {
return intersectionId;
}
}
}

View File

@@ -0,0 +1,194 @@
package sd.coordinator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sd.config.SimulationConfig;
import sd.model.Vehicle;
import sd.util.VehicleGenerator;
/**
* Tests for the Coordinator/vehicle-generation layer.
*
* What were checking here:
* 1. Coordinator can be created with a valid config
* 2. Vehicle arrival times are monotonic and sane
* 3. Vehicle IDs are created in the format we expect (V1, V2, ...)
* 4. Generated vehicles have proper routes (start at CrX, end at S)
* 5. Config actually has intersection info
* 6. Duration in config is not something crazy
*/
class CoordinatorProcessTest {
private SimulationConfig config;
private static final String TEST_CONFIG = "src/main/resources/simulation.properties";
@BeforeEach
void setUp() throws IOException {
config = new SimulationConfig(TEST_CONFIG);
}
@AfterEach
void tearDown() {
config = null;
}
/**
* Basic smoke test: can we build a coordinator with this config?
*/
@Test
void testCoordinatorInitialization() {
CoordinatorProcess coordinator = new CoordinatorProcess(config);
assertNotNull(coordinator, "Coordinator should be created with a valid config");
}
/**
* Make sure the VehicleGenerator is giving us increasing arrival times,
* i.e. time doesnt go backwards and intervals look reasonable.
*/
@Test
void testVehicleGenerationTiming() {
VehicleGenerator generator = new VehicleGenerator(config);
double currentTime = 0.0;
List<Double> arrivalTimes = new ArrayList<>();
// generate a small batch to inspect
for (int i = 0; i < 10; i++) {
double nextArrival = generator.getNextArrivalTime(currentTime);
arrivalTimes.add(nextArrival);
currentTime = nextArrival;
}
// times should strictly increase
for (int i = 1; i < arrivalTimes.size(); i++) {
assertTrue(
arrivalTimes.get(i) > arrivalTimes.get(i - 1),
"Arrival times must increase — got " + arrivalTimes.get(i - 1) + " then " + arrivalTimes.get(i)
);
}
// and they shouldn't be nonsense
for (double time : arrivalTimes) {
assertTrue(time >= 0, "Arrival time should not be negative (got " + time + ")");
assertTrue(time < 1000, "Arrival time looks suspiciously large: " + time);
}
}
/**
* We generate V1..V5 manually and make sure the IDs are exactly those.
*/
@Test
void testVehicleIdGeneration() {
VehicleGenerator generator = new VehicleGenerator(config);
List<Vehicle> vehicles = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
Vehicle v = generator.generateVehicle("V" + i, 0.0);
vehicles.add(v);
assertEquals("V" + i, v.getId(), "Vehicle ID should be 'V" + i + "' but got " + v.getId());
}
// just to be safe, no duplicates in that small set
long distinctCount = vehicles.stream().map(Vehicle::getId).distinct().count();
assertEquals(5, distinctCount, "Vehicle IDs in this batch should all be unique");
}
/**
* A generated vehicle should:
* - have a non-empty route
* - start in a known intersection (Cr1..Cr5)
* - end in S (exit)
*/
@Test
void testVehicleRouteValidity() {
VehicleGenerator generator = new VehicleGenerator(config);
for (int i = 0; i < 20; i++) {
Vehicle vehicle = generator.generateVehicle("V" + i, 0.0);
assertNotNull(vehicle.getRoute(), "Vehicle route should not be null");
assertFalse(vehicle.getRoute().isEmpty(), "Vehicle route should not be empty");
String firstHop = vehicle.getRoute().get(0);
assertTrue(
firstHop.matches("Cr[1-5]"),
"First hop should be a valid intersection (Cr1..Cr5), got: " + firstHop
);
String lastHop = vehicle.getRoute().get(vehicle.getRoute().size() - 1);
assertEquals("S", lastHop, "Last hop should be exit 'S' but got: " + lastHop);
}
}
/**
* Whatever is in simulation.properties should give us a sane duration.
*/
@Test
void testSimulationDuration() {
double duration = config.getSimulationDuration();
assertTrue(duration > 0, "Simulation duration must be positive");
assertTrue(duration >= 1.0, "Simulation should run at least 1 second (got " + duration + ")");
assertTrue(duration <= 86400.0, "Simulation should not run more than a day (got " + duration + ")");
}
/**
* Check that the 5 intersections defined in the architecture
* actually exist in the config and have valid network data.
*/
@Test
void testIntersectionConfiguration() {
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
for (String id : intersectionIds) {
String host = config.getIntersectionHost(id);
int port = config.getIntersectionPort(id);
assertNotNull(host, "Host should not be null for " + id);
assertFalse(host.isEmpty(), "Host should not be empty for " + id);
assertTrue(port > 0, "Port should be > 0 for " + id + " (got " + port + ")");
assertTrue(port < 65536, "Port should be a valid TCP port for " + id + " (got " + port + ")");
}
}
/**
* Quick sanity check: over a bunch of generated vehicles,
* we should eventually see the different vehicle types appear.
*
* Note: this is probabilistic, so we're not being super strict.
*/
@Test
void testVehicleTypeDistribution() {
VehicleGenerator generator = new VehicleGenerator(config);
boolean hasBike = false;
boolean hasLight = false;
boolean hasHeavy = false;
// 50 is enough for a "we're probably fine" test
for (int i = 0; i < 50; i++) {
Vehicle vehicle = generator.generateVehicle("V" + i, 0.0);
switch (vehicle.getType()) {
case BIKE -> hasBike = true;
case LIGHT -> hasLight = true;
case HEAVY -> hasHeavy = true;
}
}
// at least one of them should have shown up — if not, RNG is cursed
assertTrue(
hasBike || hasLight || hasHeavy,
"Expected to see at least one vehicle type after 50 generations"
);
}
}

View File

@@ -0,0 +1,140 @@
package sd.serialization;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import sd.model.Message;
import sd.model.Vehicle;
import sd.model.VehicleType;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
/**
* Test suite for JSON serialization.
*
* Tests JSON serialization to ensure:
* - Correct serialization and deserialization
* - Data integrity during round-trip conversion
* - Proper error handling
*/
class SerializationTest {
private MessageSerializer jsonSerializer = new JsonMessageSerializer();
private Vehicle testVehicle = new Vehicle("V001", VehicleType.LIGHT, 10.5,
Arrays.asList("Cr1", "Cr2", "Cr5", "S"));
private Message testMessage = new Message(
sd.model.MessageType.VEHICLE_TRANSFER,
"Cr1",
"Cr2",
testVehicle
);
// ===== JSON Serialization Tests =====
@Test
@DisplayName("JSON: Should serialize and deserialize Vehicle correctly")
void testJsonVehicleRoundTrip() throws SerializationException {
// Serialize
byte[] data = jsonSerializer.serialize(testVehicle);
assertNotNull(data);
assertTrue(data.length > 0);
// Print JSON for inspection
System.out.println("JSON Vehicle:");
System.out.println(new String(data));
// Deserialize
Vehicle deserialized = jsonSerializer.deserialize(data, Vehicle.class);
// Verify
assertNotNull(deserialized);
assertEquals(testVehicle.getId(), deserialized.getId());
assertEquals(testVehicle.getType(), deserialized.getType());
assertEquals(testVehicle.getEntryTime(), deserialized.getEntryTime());
assertEquals(testVehicle.getRoute(), deserialized.getRoute());
assertEquals(testVehicle.getTotalWaitingTime(), deserialized.getTotalWaitingTime());
assertEquals(testVehicle.getTotalCrossingTime(), deserialized.getTotalCrossingTime());
}
@Test
@DisplayName("JSON: Should serialize and deserialize Message correctly")
void testJsonMessageRoundTrip() throws SerializationException {
// Serialize
byte[] data = jsonSerializer.serialize(testMessage);
assertNotNull(data);
// Print JSON for inspection
System.out.println("\nJSON Message:");
System.out.println(new String(data));
// Deserialize
Message deserialized = jsonSerializer.deserialize(data, Message.class);
// Verify
assertNotNull(deserialized);
assertEquals(testMessage.getType(), deserialized.getType());
assertEquals(testMessage.getSenderId(), deserialized.getSenderId());
assertEquals(testMessage.getDestinationId(), deserialized.getDestinationId());
}
@Test
@DisplayName("JSON: Should throw exception on null object")
void testJsonSerializeNull() {
assertThrows(IllegalArgumentException.class, () -> {
jsonSerializer.serialize(null);
});
}
@Test
@DisplayName("JSON: Should throw exception on null data")
void testJsonDeserializeNull() {
assertThrows(IllegalArgumentException.class, () -> {
jsonSerializer.deserialize(null, Vehicle.class);
});
}
@Test
@DisplayName("JSON: Should throw exception on invalid JSON")
void testJsonDeserializeInvalid() {
byte[] invalidData = "{ invalid json }".getBytes();
assertThrows(SerializationException.class, () -> {
jsonSerializer.deserialize(invalidData, Vehicle.class);
});
}
@Test
@DisplayName("JSON: Should preserve data integrity for complex objects")
void testDataIntegrity() throws SerializationException {
// Create a more complex vehicle
Vehicle vehicle = new Vehicle("V999", VehicleType.HEAVY, 100.5,
Arrays.asList("Cr1", "Cr2", "Cr3", "Cr4", "Cr5", "S"));
vehicle.addWaitingTime(10.5);
vehicle.addWaitingTime(5.3);
vehicle.addCrossingTime(2.1);
vehicle.advanceRoute();
vehicle.advanceRoute();
// Serialize and deserialize
byte[] jsonData = jsonSerializer.serialize(vehicle);
Vehicle deserialized = jsonSerializer.deserialize(jsonData, Vehicle.class);
// Verify all fields match
assertEquals(vehicle.getId(), deserialized.getId());
assertEquals(vehicle.getType(), deserialized.getType());
assertEquals(vehicle.getTotalWaitingTime(), deserialized.getTotalWaitingTime());
assertEquals(vehicle.getCurrentRouteIndex(), deserialized.getCurrentRouteIndex());
}
// ===== Factory Tests =====
@Test
@DisplayName("Factory: Should create JSON serializer by default")
void testFactoryDefault() {
MessageSerializer serializer = SerializerFactory.createDefault();
assertNotNull(serializer);
assertEquals("JSON (Gson)", serializer.getName());
}
}