mirror of
https://github.com/davidalves04/Trabalho-Pratico-SD.git
synced 2025-12-08 20:43:32 +00:00
Compare commits
33 Commits
v0.2.2
...
12-impleme
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c256ad6f5 | |||
| 340e436063 | |||
| 1684a6713e | |||
| 22a7081ade | |||
| 3b699556db | |||
|
|
d078808486 | ||
|
|
98581b562d | ||
| f9644bd18c | |||
|
|
c6b710ac52 | ||
|
|
dc4f567e1f | ||
|
|
db5e01021a | ||
|
|
dab0651dbd | ||
|
|
4772add574 | ||
|
|
ae27115791 | ||
|
|
684fb408ef | ||
|
|
d057adeab3 | ||
|
|
be4e7f66d6 | ||
| fd26063f6e | |||
|
|
d8b59cc502 | ||
|
|
06c34a198a | ||
|
|
1524188b29 | ||
|
|
bc1a8da160 | ||
|
|
96903e4b7c | ||
|
|
6c5eab0e72 | ||
| 23f7a74798 | |||
| d7dec0d73e | |||
|
|
534a880e3e | ||
|
|
ba3233eae1 | ||
|
|
d20040835c | ||
|
|
2399b4b472 | ||
|
|
974debf7db | ||
|
|
3fe467a2a3 | ||
|
|
af9b091e76 |
620
README.md
Normal file
620
README.md
Normal 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 (1º 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
|
||||||
41
main/pom.xml
41
main/pom.xml
@@ -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>
|
||||||
501
main/src/main/java/sd/IntersectionProcess.java
Normal file
501
main/src/main/java/sd/IntersectionProcess.java
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
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.model.Intersection;
|
||||||
|
import sd.model.MessageType;
|
||||||
|
import sd.model.TrafficLight;
|
||||||
|
import sd.model.TrafficLightState;
|
||||||
|
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.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
startTrafficLights();
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
trafficLightPool.submit(() -> runTrafficLightCycle(light));
|
||||||
|
System.out.println(" Started thread for: " + light.getDirection());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main loop for a traffic light thread.
|
||||||
|
* Continuously cycles between GREEN and RED states.
|
||||||
|
*
|
||||||
|
* @param light The traffic light to control.
|
||||||
|
*/
|
||||||
|
private void runTrafficLightCycle(TrafficLight light) {
|
||||||
|
System.out.println("[" + light.getId() + "] Traffic light thread started.");
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
// Green state
|
||||||
|
light.changeState(TrafficLightState.GREEN);
|
||||||
|
System.out.println("[" + light.getId() + "] State: GREEN");
|
||||||
|
|
||||||
|
// Process vehicles while green
|
||||||
|
processGreenLight(light);
|
||||||
|
|
||||||
|
// Wait for green duration
|
||||||
|
Thread.sleep((long) (light.getGreenTime() * 1000));
|
||||||
|
|
||||||
|
// RED state
|
||||||
|
light.changeState(TrafficLightState.RED);
|
||||||
|
System.out.println("[" + light.getId() + "] State: RED");
|
||||||
|
|
||||||
|
// Wait for red duration
|
||||||
|
Thread.sleep((long) (light.getRedTime() * 1000));
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes vehicles when a traffic light is GREEN.
|
||||||
|
* Dequeues vehicles and sends them to their next destination.
|
||||||
|
*
|
||||||
|
* @param light The traffic light that is currently green.
|
||||||
|
*/
|
||||||
|
private void processGreenLight(TrafficLight light) {
|
||||||
|
while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
|
||||||
|
Vehicle vehicle = light.removeVehicle();
|
||||||
|
|
||||||
|
if (vehicle != null) {
|
||||||
|
// Get crossing time based on vehicle type
|
||||||
|
double crossingTime = getCrossingTimeForVehicle(vehicle);
|
||||||
|
|
||||||
|
// Simulate crossing time
|
||||||
|
try {
|
||||||
|
Thread.sleep((long) (crossingTime * 1000));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update vehicle statistics
|
||||||
|
vehicle.addCrossingTime(crossingTime);
|
||||||
|
|
||||||
|
// Update intersection statistics
|
||||||
|
intersection.incrementVehiclesSent();
|
||||||
|
|
||||||
|
// Send vehicle to next destination
|
||||||
|
sendVehicleToNextDestination(vehicle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the crossing time for a vehicle based on its type.
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a vehicle to its next destination via socket connection.
|
||||||
|
*
|
||||||
|
* @param vehicle The vehicle that has crossed this intersection.
|
||||||
|
*/
|
||||||
|
private 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);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
204
main/src/main/java/sd/coordinator/CoordinatorProcess.java
Normal file
204
main/src/main/java/sd/coordinator/CoordinatorProcess.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
124
main/src/main/java/sd/coordinator/SocketClient.java
Normal file
124
main/src/main/java/sd/coordinator/SocketClient.java
Normal 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 won’t 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)) {
|
||||||
|
|||||||
142
main/src/main/java/sd/model/Message.java
Normal file
142
main/src/main/java/sd/model/Message.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
main/src/main/java/sd/model/MessageType.java
Normal file
81
main/src/main/java/sd/model/MessageType.java
Normal 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
|
||||||
|
}
|
||||||
41
main/src/main/java/sd/protocol/MessageProtocol.java
Normal file
41
main/src/main/java/sd/protocol/MessageProtocol.java
Normal 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();
|
||||||
|
}
|
||||||
199
main/src/main/java/sd/protocol/SocketConnection.java
Normal file
199
main/src/main/java/sd/protocol/SocketConnection.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
114
main/src/main/java/sd/serialization/JsonMessageSerializer.java
Normal file
114
main/src/main/java/sd/serialization/JsonMessageSerializer.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
main/src/main/java/sd/serialization/MessageSerializer.java
Normal file
48
main/src/main/java/sd/serialization/MessageSerializer.java
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
134
main/src/main/java/sd/serialization/SerializationExample.java
Normal file
134
main/src/main/java/sd/serialization/SerializationExample.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
main/src/main/java/sd/serialization/SerializerFactory.java
Normal file
66
main/src/main/java/sd/serialization/SerializerFactory.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
473
main/src/test/java/IntersectionProcessTest.java
Normal file
473
main/src/test/java/IntersectionProcessTest.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 we’re 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
194
main/src/test/java/sd/coordinator/CoordinatorProcessTest.java
Normal file
194
main/src/test/java/sd/coordinator/CoordinatorProcessTest.java
Normal 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 we’re 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 doesn’t 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
main/src/test/java/sd/serialization/SerializationTest.java
Normal file
140
main/src/test/java/sd/serialization/SerializationTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user