1 Commits

Author SHA1 Message Date
Leandro Afonso
70296cf56f old files 2025-10-13 20:54:15 +01:00
79 changed files with 3881 additions and 7057 deletions

View File

@@ -1,61 +0,0 @@
name: Java CI with Maven
on:
push:
branches: [ "main" ]
tags:
- 'v*.*.*'
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package
working-directory: main
- name: Upload built JAR
uses: actions/upload-artifact@v4
with:
name: package
path: main/target/*.jar
- name: Generate dependency graph
run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph
- name: Upload dependency graph artifact
uses: actions/upload-artifact@v4
with:
name: dependency-graph
path: main/target/**
publish-release:
runs-on: ubuntu-latest
needs: [build]
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download built JAR
uses: actions/download-artifact@v4
with:
name: package
path: main/target/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: main/target/*.jar

50
.gitignore vendored
View File

@@ -1,50 +0,0 @@
# Compiled class files
*.class
# Log files
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# VS Code settings
.vscode/
# Eclipse files
*.pydevproject
.project
.classpath
.cproject
.settings/
bin/
tmp/
# IntelliJ IDEA files
*.iml
.idea/
out/
# Mac system files
.DS_Store
# Windows system files
Thumbs.db
# Maven
target/
# Gradle
.gradle/
build/
# Other
*.swp
*.pdf

View File

@@ -1,27 +0,0 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" version="28.2.7">
<diagram name="Página-1" id="B1_hHcevBzWlEwI7FSV6">
<mxGraphModel dx="778" dy="476" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="vcp7vux32DhQR4tKQhnF-8" value="Dashboard" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=#C73500;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;fillColor=#fa6800;shape=mxgraph.mscae.oms.dashboard;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="389" y="230" width="50" height="41" as="geometry" />
</mxCell>
<mxCell id="vcp7vux32DhQR4tKQhnF-12" value="Semaforo.java" style="shape=image;html=1;verticalAlign=top;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;imageAspect=0;aspect=fixed;image=https://icons.diagrams.net/icon-cache1/Strabo-2829/traffic_light-1068.png" vertex="1" parent="1">
<mxGeometry x="230" y="350" width="53" height="53" as="geometry" />
</mxCell>
<mxCell id="vcp7vux32DhQR4tKQhnF-13" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;movable=1;resizable=1;rotatable=1;deletable=1;editable=1;locked=0;connectable=1;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="310" y="330" as="sourcePoint" />
<mxPoint x="360" y="280" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="vcp7vux32DhQR4tKQhnF-14" value="CruzamentoServer.java" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=1;points=[];movable=1;rotatable=1;deletable=1;editable=1;locked=0;connectable=1;" vertex="1" connectable="0" parent="vcp7vux32DhQR4tKQhnF-13">
<mxGeometry x="-0.3933" relative="1" as="geometry">
<mxPoint x="25" y="25" as="offset" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

620
README.md
View File

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

View File

@@ -1,134 +0,0 @@
# 🏁 Single-Process Prototype — Implementation Summary
**Status:** ✅ Complete
**Date:** October 22, 2025
**Branch:** `8-single-process-prototype`
---
## Overview
The single-process prototype implements a **discrete event simulation (DES)** of a 3×3 urban grid with five intersections, realistic vehicle behavior, and fully synchronized traffic lights. Everything runs under one process, laying the groundwork for the distributed architecture in Phase 3.
---
## Core Architecture
### **SimulationEngine**
Drives the DES loop with a priority queue of timestamped events — vehicles, lights, crossings, and periodic stats updates. Handles five intersections (Cr1Cr5) and six event types.
**Main loop:**
```
while (events && time < duration):
event = nextEvent()
time = event.timestamp
handle(event)
```
### **VehicleGenerator**
Spawns vehicles via:
* **Poisson arrivals** (λ = 0.5 veh/s) or fixed intervals
* **Probabilistic routes** from E1E3
* **Type distribution**: 20% BIKE, 60% LIGHT, 20% HEAVY
### **StatisticsCollector**
Tracks system-wide and per-type metrics: throughput, avg. wait, queue sizes, light cycles — updated every 10 s and at simulation end.
---
## Model Highlights
* **Vehicle** type, route, timings, lifecycle.
* **Intersection** routing tables, traffic lights, queues.
* **TrafficLight** red/green cycles with FIFO queues.
* **Event** timestamped, comparable; 6 types for all DES actions.
---
## Configuration (`simulation.properties`)
```properties
simulation.duration=60.0
simulation.arrival.model=POISSON
simulation.arrival.rate=0.5
vehicle.bike.crossingTime=1.5
vehicle.light.crossingTime=2.0
vehicle.heavy.crossingTime=4.0
statistics.update.interval=10.0
```
**Speed logic:**
`t_bike = 0.5×t_car`, `t_heavy = 2×t_car`.
---
## Topology
```
E1→Cr1→Cr4→Cr5→S
E2→Cr2→Cr5→S
E3→Cr3→S
Bi-dir: Cr1↔Cr2, Cr2↔Cr3
```
---
## Results
**Unit Tests:** 7/7 ✅
**60-Second Simulation:**
* Generated: 22 vehicles
* Completed: 5 (22.7%)
* Avg system time: 15.47 s
* Throughput: 0.08 veh/s
* All lights & intersections operational
**Performance:**
~0.03 s real-time run (≈2000× speed-up), < 50 MB RAM.
---
## Code Structure
```
sd/
├── engine/SimulationEngine.java
├── model/{Vehicle,Intersection,TrafficLight,Event}.java
├── util/{VehicleGenerator,StatisticsCollector}.java
└── config/SimulationConfig.java
```
---
## Key Flow
1. Initialize intersections, lights, first events.
2. Process events chronologically.
3. Vehicles follow routes queue cross exit.
4. Lights toggle, queues drain, stats update.
5. Print summary and performance metrics.
---
## Next Steps — Phase 3
* Split intersections into independent **processes**.
* Add **socket-based communication**.
* Run **traffic lights as threads**.
* Enable **distributed synchronization** and fault handling.
---
## TL;DR
Solid single-process DES
Everythings working traffic lights, routing, vehicles, stats.
Ready to go distributed next.

198
TODO.md
View File

@@ -1,198 +0,0 @@
## ✅ SINGLE-PROCESS PROTOTYPE - COMPLETED
### Phase 2 Status: DONE ✅
All components for the single-process prototype have been successfully implemented and tested:
-**SimulationEngine** - Priority queue-based discrete event simulation
-**VehicleGenerator** - Poisson and Fixed arrival models
-**StatisticsCollector** - Comprehensive metrics tracking
-**Entry point** - Main simulation runner
-**60s test simulation** - Successfully validated event processing and routing
### Test Results:
- All 7 unit tests passing
- 60-second simulation completed successfully
- Generated 22 vehicles with 5 completing their routes
- Traffic light state changes working correctly
- Vehicle routing through intersections validated
---
## NEXT: Distributed Architecture Implementation
### Compreender os Conceitos Fundamentais
Primeiro, as tecnologias e paradigmas chave necessários para este projeto devem ser totalmente compreendidos.
- **Processos vs. Threads:** O projeto especifica o uso de ambos.
- **Processos (para Cruzamentos)** são programas independentes, cada um com o seu próprio espaço de memória. Em Java, cada cruzamento será provavelmente executado como uma aplicação Java separada (uma instância distinta da JVM).
- **Threads (para Semáforos)** existem _dentro_ de um processo e partilham memória. Isto é adequado para os semáforos, pois eles precisam de ser coordenados e partilhar dados (como filas de veículos) dentro do mesmo cruzamento.
- **Comunicação Entre Processos (IPC - Inter-Process Communication):** Como os cruzamentos são processos separados, é necessário um método para que eles comuniquem. **Sockets** são o método especificado. Quando um veículo sai de um cruzamento (ex: `Cr1`) e vai para outro (ex: `Cr2`), o processo `Cr1` precisa de enviar uma mensagem contendo os dados do veículo para o processo `Cr2` através de uma conexão por socket.
- **Simulação de Eventos Discretos (DES - Discrete-Event Simulation):** Este é o paradigma de simulação que deve ser utilizado. Em vez de o tempo fluir continuamente, o relógio da simulação salta de um evento para o seguinte.
- Um **evento** é um objeto que representa algo que acontece num ponto específico no tempo (ex: "Veículo A chega ao Cr2 no tempo 15.7s").
- Uma **lista de eventos** central, frequentemente uma fila de prioridades, será necessária para armazenar eventos futuros, ordenados pelo seu timestamp. O ciclo principal da simulação retira o próximo evento da lista, processa-o e adiciona quaisquer novos eventos que resultem dele.
- **Processo de Poisson:** Para o modelo 'mais realista' de chegadas de veículos, é especificado um processo de Poisson. A principal conclusão é que o tempo _entre_ chegadas consecutivas de veículos segue uma **distribuição exponencial**. Em Java, este intervalo pode ser gerado usando `Math.log(1 - Math.random()) / -lambda`, onde `lambda` (λi) é a taxa de chegada especificada.
---
### Uma Sugestão de Arquitetura de Alto Nível
Abaixo, é apresentada uma possível estrutura para a aplicação distribuída. Pode ser vista como um conjunto de programas independentes que comunicam através de uma rede.
1. **Processo Coordenador/Gerador (1 Processo):**
- **Propósito:** Iniciar a simulação, gerar veículos e gerir o relógio global da simulação ou os critérios de paragem.
- **Responsabilidades:**
- Lê a configuração da simulação (ex: carga de tráfego λi, tempos dos semáforos).
- Gera veículos de acordo com o modelo selecionado (intervalo fixo ou processo de Poisson).
- Atribui a cada novo veículo um percurso com base na distribuição uniforme especificada.
- Injeta o veículo no sistema enviando uma mensagem para o primeiro processo de cruzamento no seu percurso (ex: de um ponto de entrada E1 para Cr1).
2. **Processos de Cruzamento (5 Processos):**
- **Propósito:** Simular cada cruzamento (`Cr1` a `Cr5`) como um processo distinto.
- **Responsabilidades:**
- Escuta por veículos a chegar de outros processos.
- Gere as filas de veículos para os seus semáforos.
- Executa múltiplas **threads de Semáforo** internamente.
- Coordena estas threads para garantir que apenas uma direção de tráfego está aberta a cada momento.
- Quando um veículo atravessa, é encaminhado para o processo seguinte no seu percurso.
- Envia periodicamente as suas estatísticas (ex: comprimentos atuais das filas) para o Servidor do Dashboard.
3. **Processo de Nó de Saída (1 Processo):**
- **Propósito:** Representar o ponto de saída `S` e atuar como um coletor de dados para estatísticas globais.
- **Responsabilidades:**
- Recebe veículos que completaram o seu percurso.
- Calcula métricas globais como o tempo total de viagem (tempo de permanência) para cada veículo.
- Agrega e calcula as estatísticas finais (ex: tempo de viagem mínimo, máximo e médio por tipo de veículo).
- Envia estas estatísticas globais para o Servidor do Dashboard.
4. **Processo do Servidor do Dashboard (1 Processo):**
- **Propósito:** Agregar e exibir todos os dados da simulação em tempo real.
- **Responsabilidades:**
- Abre um socket de servidor e escuta por dados a chegar de todos os processos de Cruzamento e de Saída.
- Armazena e atualiza as estatísticas à medida que chegam.
- Apresenta os dados numa interface de utilizador, que deve exibir métricas e ser atualizada durante a simulação.
---
### Plano
Nem tudo deve ser construído de uma só vez. Os seguintes passos incrementais são recomendados.
#### **Passo 1: Modelação e Classes Principais (Não-distribuído)**
Antes de escrever qualquer lógica complexa, as estruturas de dados devem ser definidas. Devem ser criados Plain Old Java Objects (POJOs) para:
- `Veiculo`: Com atributos como um identificador único, tipo, tempo de entrada e o percurso realizado. Deve ser tornado `Serializable` para que possa ser enviado através de sockets.
- `Evento`: Com atributos como um timestamp e o tipo de evento (ex: `VEHICLE_ARRIVAL`), bem como dados associados.
- `Semaforo`: Para conter o seu estado (`VERDE`/`VERMELHO`) e a fila de veículos.
- `Cruzamento`: Para conter os seus semáforos e a lógica operacional.
#### **Passo 2: Construir um Protótipo de Processo Único**
Este é um passo crucial. Sockets e processos devem ser deixados de lado por agora para construir toda a simulação numa única aplicação Java.
- Deve ser criado um ciclo de simulação central baseado numa fila de prioridades para objetos `Evento`.
- Todos os objetos `Cruzamento` e `Semaforo` devem ser instanciados.
- A lógica principal deve ser tornada funcional: veículos a moverem-se entre filas, semáforos a mudar de estado e estatísticas básicas a serem recolhidas.
- **Objetivo:** Uma simulação totalmente funcional e não-distribuída. Isto torna a depuração significativamente mais fácil.
#### **Passo 3: Distribuir os Cruzamentos**
O protótipo pode agora ser convertido num sistema distribuído.
- A classe `Cruzamento` deve ser tornada executável como uma aplicação Java autónoma (com um método `main`). Serão lançadas cinco instâncias, uma para cada cruzamento.
- Devem ser configurados sockets TCP para comunicação. Cada processo de cruzamento precisa de saber o endereço/porta dos vizinhos para os quais pode enviar veículos.
- Um **protocolo de comunicação** claro deve ser definido. Por exemplo, quando `Cr1` envia um veículo para `Cr2`, o objeto `Veiculo` é serializado e escrito no socket conectado a `Cr2`. O processo `Cr2` terá uma thread dedicada para escutar estas conexões de entrada.
#### **Passo 4: Implementar as Threads dos Semáforos**
Dentro de cada processo `Cruzamento`, os semáforos devem ser implementados como threads.
- O principal desafio aqui é a **sincronização**. As threads dos semáforos num único cruzamento partilham as filas de veículos.
- As ferramentas de concorrência do Java (como `synchronized`, `ReentrantLock`, `Semaphore`) devem ser usadas para garantir que apenas um semáforo pode estar verde para um percurso conflituante e que o acesso às filas partilhadas é seguro (thread-safe).
#### **Passo 5: Implementar o Dashboard**
- O processo `DashboardServer` deve ser criado. Ele irá escutar numa porta específica por estatísticas a chegar.
- Nos processos `Cruzamento` e `Saida`, deve ser adicionado um mecanismo para enviar periodicamente um resumo das suas estatísticas atuais para o Servidor do Dashboard.
- A UI deve ser construída para exibir estes dados em tempo real.
#### **Passo 6: Testes e Análise**
Assim que o sistema completo estiver a funcionar, as experiências exigidas pela descrição do projeto podem ser realizadas.
- A simulação deve ser executada com diferentes taxas de chegada de veículos para simular cargas baixas, médias e altas.
- Diferentes políticas de temporização dos semáforos devem ser testadas para medir o seu impacto no congestionamento.
- Diferentes algoritmos de seleção de percurso e o seu impacto no desempenho do sistema devem ser avaliados.
- Para cada cenário, a simulação deve ser executada várias vezes para recolher estatísticas fiáveis (médias, desvios padrão, intervalos de confiança), conforme solicitado.
#### **Passo 7: Escrever o Relatório**
À medida que cada passo é concluído, deve ser documentado. Isto tornará a escrita do relatório final muito mais fácil. Todos os pontos mencionados nas secções "Entrega" e "Critérios de Avaliação" devem ser abordados.
---
### OBS:
- **Começar de Forma Simples:** O protótipo de processo único (Passo 2) evitará grandes dificuldades mais tarde.
- **Protocolo de Comunicação:** O protocolo de mensagens deve ser definido o mais cedo possível. A informação exata que um processo envia para outro deve ser clara//simples//consistente.
- **Debugging:** Debugging de sistemas distribuídos podem ser difíceis. Uma framework de logging (como Log4j 2 ou SLF4J) pode ser usada para registar eventos//alterações de estado nos diferentes processos.
- **Configuração:** Valores como endereços IP, números de porta ou parâmetros da simulação não devem ser "hardcoded". Um ficheiro de configuração (ex: um ficheiro `.properties` ou `.json`) torna a aplicação mais fácil de executar e testar.

View File

@@ -1,172 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<mxfile host="app.diagrams.net" agent="Gemini" version="28.2.7">
<diagram name="Arquitetura-Sistema-Trafego" id="L-jWkP8vD7q_2fM6N-yC">
<mxGraphModel dx="1434" dy="746" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="E1-process" value="Processo Gerador E1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="140" y="100" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="E2-process" value="Processo Gerador E2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="430" y="100" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="E3-process" value="Processo Gerador E3" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="720" y="100" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="Cr1-process" value="Processo Cruzamento (Cr1)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="140" y="240" width="140" height="100" as="geometry" />
</mxCell>
<mxCell id="Cr1-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr1-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr1-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr1-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr2-process" value="Processo Cruzamento (Cr2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="430" y="240" width="140" height="140" as="geometry" />
</mxCell>
<mxCell id="Cr2-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr2-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr2-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr2-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr2-thread3" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr2-process">
<mxGeometry x="20" y="100" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr3-process" value="Processo Cruzamento (Cr3)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="720" y="240" width="140" height="140" as="geometry" />
</mxCell>
<mxCell id="Cr3-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr3-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr3-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr3-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr3-thread3" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr3-process">
<mxGeometry x="20" y="100" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr4-process" value="Processo Cruzamento (Cr4)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="140" y="460" width="140" height="100" as="geometry" />
</mxCell>
<mxCell id="Cr4-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr4-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr4-thread-peao" value="Thread Semáforo (Peões)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr4-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr5-process" value="Processo Cruzamento (Cr5)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="430" y="460" width="140" height="100" as="geometry" />
</mxCell>
<mxCell id="Cr5-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr5-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr5-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr5-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="S-process" value="Processo Saída (S)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="720" y="460" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="dashboard-server" value="Servidor Dashboard" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="430" y="640" width="140" height="80" as="geometry" />
</mxCell>
<mxCell id="arrow-E1-Cr1" value="Fluxo Veículos&lt;br&gt;(Sockets/Middleware)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;fontSize=10;align=left;verticalAlign=bottom;" edge="1" parent="1" source="E1-process" target="Cr1-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-E2-Cr2" value="Fluxo Veículos&lt;br&gt;(Sockets/Middleware)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;fontSize=10;align=left;verticalAlign=bottom;" edge="1" parent="1" source="E2-process" target="Cr2-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-E3-Cr3" value="Fluxo Veículos&lt;br&gt;(Sockets/Middleware)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;fontSize=10;align=left;verticalAlign=bottom;" edge="1" parent="1" source="E3-process" target="Cr3-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr1-Cr4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr1-process" target="Cr4-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr2-Cr5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr2-process" target="Cr5-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr4-Cr5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr4-process" target="Cr5-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr5-S" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr5-process" target="S-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr3-S" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr3-process" target="S-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr1-Cr2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr1-process" target="Cr2-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="280" y="290" />
<mxPoint x="430" y="290" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow-Cr2-Cr1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr2-process" target="Cr1-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="430" y="310" />
<mxPoint x="280" y="310" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow-Cr2-Cr3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr2-process" target="Cr3-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="570" y="290" />
<mxPoint x="720" y="290" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow-Cr3-Cr2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr3-process" target="Cr2-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="720" y="310" />
<mxPoint x="570" y="310" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr1-Dash" value="Envio de Estatísticas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="Cr1-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="210" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr2-Dash" value="Envio de Estatísticas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="Cr2-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="stats-Cr3-Dash" value="Envio de Estatísticas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="Cr3-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="790" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr4-Dash" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;" edge="1" parent="1" source="Cr4-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="210" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr5-Dash" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;" edge="1" parent="1" source="Cr5-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="stats-S-Dash" value="Estatísticas Globais" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="S-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="790" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="legend" value="Legenda simplificada (removida tabela)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1">
</mxCell>
<mxCell id="title" value="Diagrama de Arquitetura - Simulador de Tráfego Distribuído" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="290" y="40" width="420" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -11,58 +11,6 @@
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Gson for JSON serialization -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Exec Plugin for running examples -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>sd.Entry</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>sd.Entry</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -0,0 +1,147 @@
package client;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.structs.NetworkManager;
import client.handlers.UnicastHandler;
import client.handlers.MulticastHandler;
import client.handlers.BroadcastHandler;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Client application that manages network communications and thread handling.
* Created by: 0x1eo
* Last modified: 2024-12-12
*/
public class Client implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(Client.class.getName());
public static final String SERVER_ADDRESS = "localhost";
public static final String BROADCAST_ADDRESS = "255.255.255.255";
public static final int SERVER_PORT = 7500;
public static int CLIENT_PORT = 7501; // Made non-final to allow dynamic assignment
public static final int MULTICAST_PORT = 7502;
public static final int BUFFER_SIZE = 1024;
private final ExecutorService executorService;
private final NetworkManager networkManager;
private static Client instance;
private Client() {
this.executorService = Executors.newFixedThreadPool(3);
this.networkManager = NetworkManager.getInstance();
}
public static synchronized Client getInstance() {
if (instance == null) {
instance = new Client();
}
return instance;
}
/**
* Initializes and starts the client application.
*/
public void start() {
LOGGER.info("Initializing client application...");
try {
networkManager.initializePrimaryConnection();
setupShutdownHook();
initializeAuthenticatedState();
startInputLoop();
LOGGER.info("Client initialization completed successfully");
} catch (Exception e) {
LOGGER.severe("Failed to initialize client: " + e.getMessage());
close();
}
}
/**
* Initializes authenticated state and starts network handlers.
*/
public void initializeAuthenticatedState() {
try {
NetworkManager networkManager = NetworkManager.getInstance();
networkManager.initializeAuthenticatedConnections();
if (networkManager.isAuthenticated()) {
startNetworkHandlers();
LOGGER.info("Authenticated state initialized successfully on port " + CLIENT_PORT);
} else {
LOGGER.severe("Authentication failed");
close();
}
} catch (NetworkManager.NetworkInitializationException e) {
LOGGER.log(Level.SEVERE, "Failed to initialize authenticated state", e);
close();
}
}
private void startNetworkHandlers() {
executorService.execute(new UnicastHandler());
executorService.execute(new MulticastHandler());
executorService.execute(new BroadcastHandler());
}
private void setupShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
LOGGER.info("Shutdown hook triggered");
close();
}));
}
@Override
public void close() {
LOGGER.info("Initiating client shutdown sequence...");
shutdownExecutors();
networkManager.close();
LOGGER.info("Client shutdown completed");
System.exit(0);
}
private void shutdownExecutors() {
try {
UnicastHandler.getExecutorService().shutdown();
executorService.shutdown();
// Wait for termination
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
LOGGER.warning("Executor service shutdown interrupted: " + e.getMessage());
Thread.currentThread().interrupt();
} catch (Exception e) {
LOGGER.severe("Error during executor service shutdown: " + e.getMessage());
}
}
private void startInputLoop() {
Thread inputThread = new Thread(() -> {
try (Scanner scanner = new Scanner(System.in)) {
while (!Thread.currentThread().isInterrupted()) {
String input = scanner.nextLine();
String response = InputCommandRouter.processInput(ConnType.UNICAST, input);
if (response != null) {
networkManager.getUnicastOut().println(response);
}
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error in input loop", e);
}
});
inputThread.setName("InputProcessor");
inputThread.start();
}
public static void main(String[] args) {
Client client = Client.getInstance();
client.start();
}
}

View File

@@ -0,0 +1,164 @@
package client.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.Client;
import client.structs.NetworkManager;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Handles broadcast communication across the network.
* Receives and processes broadcast packets, excluding packets from the local host.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class BroadcastHandler implements Runnable, AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(BroadcastHandler.class.getName());
private static final int SOCKET_TIMEOUT_MS = 5000; // 5 seconds
private static final long TIMEOUT_LOG_INTERVAL = 300000; // Log timeouts every 5 minutes
private static final long HEARTBEAT_INTERVAL = 60000; // 1 minute
private final NetworkManager networkManager;
private volatile boolean isRunning;
private DatagramSocket socket;
private long lastTimeoutLog;
private long lastHeartbeat;
public BroadcastHandler() {
this.networkManager = NetworkManager.getInstance();
this.isRunning = true;
this.lastTimeoutLog = System.currentTimeMillis();
this.lastHeartbeat = System.currentTimeMillis();
}
@Override
public void run() {
try {
initializeSocket();
LOGGER.info("Broadcast handler started successfully on port " + socket.getLocalPort());
processBroadcastMessages();
} catch (IOException e) {
if (isRunning) {
LOGGER.log(Level.SEVERE, "Fatal error in broadcast handler", e);
}
} finally {
close();
}
}
private void initializeSocket() throws IOException {
this.socket = networkManager.getBroadcastSocket();
if (socket == null) {
throw new IOException("Failed to initialize broadcast socket");
}
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
LOGGER.fine("Broadcast socket timeout set to " + SOCKET_TIMEOUT_MS + "ms");
}
private void processBroadcastMessages() throws IOException {
byte[] buffer = new byte[Client.BUFFER_SIZE];
InetAddress localhost = InetAddress.getLocalHost();
while (isRunning) {
checkHeartbeat();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
receiveAndProcessPacket(packet, localhost);
} catch (IOException e) {
handleReceiveException(e);
}
}
}
private void handleReceiveException(IOException e) {
if (!isRunning) return;
if (e instanceof java.net.SocketTimeoutException) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastTimeoutLog > TIMEOUT_LOG_INTERVAL) {
LOGGER.fine("No broadcast messages received in the last " +
(SOCKET_TIMEOUT_MS / 1000) + " seconds");
lastTimeoutLog = currentTime;
}
} else {
LOGGER.log(Level.WARNING, "Error receiving broadcast packet", e);
}
}
private void checkHeartbeat() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastHeartbeat > HEARTBEAT_INTERVAL) {
if (socket != null && !socket.isClosed()) {
LOGGER.fine("Broadcast connection alive - listening for messages");
}
lastHeartbeat = currentTime;
}
}
private void receiveAndProcessPacket(DatagramPacket packet, InetAddress localhost) throws IOException {
socket.receive(packet);
if (packet.getAddress().equals(localhost)) {
return; // Skip localhost packets
}
String input = extractMessage(packet);
String output = processMessage(input);
if (output != null) {
sendResponse(output, packet.getAddress(), packet.getPort());
}
}
private String extractMessage(DatagramPacket packet) {
return new String(
packet.getData(),
0,
packet.getLength(),
StandardCharsets.UTF_8
).trim();
}
private String processMessage(String input) {
try {
return InputCommandRouter.processInput(ConnType.BROADCAST, input);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error processing message", e);
return null;
}
}
private void sendResponse(String message, InetAddress address, int port) {
try {
byte[] responseData = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket response = new DatagramPacket(
responseData,
responseData.length,
address,
port
);
socket.send(response);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to send broadcast response", e);
}
}
@Override
public void close() {
isRunning = false;
if (socket != null && !socket.isClosed()) {
socket.close();
LOGGER.info("Broadcast handler closed");
}
}
}

View File

@@ -0,0 +1,163 @@
package client.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.Client;
import client.structs.NetworkManager;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Handles multicast communication between network nodes.
* Receives and processes multicast packets, excluding packets from the local host.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class MulticastHandler implements Runnable, AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(MulticastHandler.class.getName());
private static final int SOCKET_TIMEOUT_MS = 5000; // 5 seconds
private static final long TIMEOUT_LOG_INTERVAL = 300000; // Log timeouts every 5 minutes
private static final long HEARTBEAT_INTERVAL = 60000; // 1 minute
private final NetworkManager networkManager;
private volatile boolean isRunning;
private DatagramSocket socket;
private long lastTimeoutLog;
private long lastHeartbeat;
public MulticastHandler() {
this.networkManager = NetworkManager.getInstance();
this.isRunning = true;
this.lastTimeoutLog = System.currentTimeMillis();
this.lastHeartbeat = System.currentTimeMillis();
}
@Override
public void run() {
try {
initializeSocket();
LOGGER.info("Multicast handler started successfully on port " + socket.getLocalPort());
processMulticastMessages();
} catch (IOException e) {
if (isRunning) {
LOGGER.log(Level.SEVERE, "Fatal error in multicast handler", e);
}
} finally {
close();
}
}
private void initializeSocket() throws IOException {
this.socket = networkManager.getMulticastSocket();
if (socket == null) {
throw new IOException("Failed to initialize multicast socket");
}
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
LOGGER.fine("Multicast socket timeout set to " + SOCKET_TIMEOUT_MS + "ms");
}
private void processMulticastMessages() throws IOException {
byte[] buffer = new byte[Client.BUFFER_SIZE];
InetAddress localhost = InetAddress.getLocalHost();
while (isRunning) {
checkHeartbeat();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
receiveAndProcessPacket(packet, localhost);
} catch (IOException e) {
handleReceiveException(e);
}
}
}
private void handleReceiveException(IOException e) {
if (!isRunning) return;
if (e instanceof java.net.SocketTimeoutException) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastTimeoutLog > TIMEOUT_LOG_INTERVAL) {
LOGGER.fine("No multicast messages received in the last " +
(SOCKET_TIMEOUT_MS / 1000) + " seconds");
lastTimeoutLog = currentTime;
}
} else {
LOGGER.log(Level.WARNING, "Error receiving multicast packet", e);
}
}
private void checkHeartbeat() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastHeartbeat > HEARTBEAT_INTERVAL) {
if (socket != null && !socket.isClosed()) {
LOGGER.fine("Multicast connection alive - listening for messages");
}
lastHeartbeat = currentTime;
}
}
private void receiveAndProcessPacket(DatagramPacket packet, InetAddress localhost) throws IOException {
socket.receive(packet);
if (packet.getAddress().equals(localhost)) {
return; // Skip localhost packets
}
String input = extractMessage(packet);
String output = processMessage(input);
if (output != null) {
sendResponse(output, packet.getAddress(), packet.getPort());
}
}
private String extractMessage(DatagramPacket packet) {
return new String(
packet.getData(),
0,
packet.getLength(),
StandardCharsets.UTF_8
).trim();
}
private String processMessage(String input) {
try {
return InputCommandRouter.processInput(ConnType.MULTICAST, input);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error processing message", e);
return null;
}
}
private void sendResponse(String message, InetAddress address, int port) {
try {
byte[] responseData = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket response = new DatagramPacket(
responseData,
responseData.length,
address,
port
);
socket.send(response);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to send multicast response", e);
}
}
@Override
public void close() {
isRunning = false;
if (socket != null && !socket.isClosed()) {
socket.close();
LOGGER.info("Multicast handler closed");
}
}
}

View File

@@ -0,0 +1,143 @@
package client.handlers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.structs.NetworkManager;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Handles unicast (point-to-point) connections between clients.
* Manages incoming connections and processes their messages in separate threads.
*
* @author 0x1eo
* @since 2024-12-12
*/
public class UnicastHandler implements Runnable, AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(UnicastHandler.class.getName());
private static final int MAX_THREADS = 50;
private static final int SOCKET_TIMEOUT_MS = 30000; // 30 seconds
private static final int SHUTDOWN_TIMEOUT_SECONDS = 5;
private static ExecutorService connectionPool = null;
private final NetworkManager networkManager;
private volatile boolean isRunning;
/**
* Creates a new UnicastHandler with a fixed thread pool.
*/
public UnicastHandler() {
connectionPool = Executors.newFixedThreadPool(MAX_THREADS);
this.networkManager = NetworkManager.getInstance();
this.isRunning = true;
}
@Override
public void run() {
ServerSocket serverSocket = networkManager.getServerSocket();
if (serverSocket == null) {
LOGGER.severe("Server socket is null. Cannot start UnicastHandler.");
return;
}
while (isRunning) {
try {
Socket clientSocket = serverSocket.accept();
configureSocket(clientSocket);
connectionPool.execute(new ClientHandler(clientSocket));
} catch (IOException e) {
if (isRunning) {
LOGGER.log(Level.SEVERE, "Error accepting connection", e);
}
}
}
}
private void configureSocket(Socket socket) throws IOException {
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
socket.setKeepAlive(true);
}
@Override
public void close() {
isRunning = false;
shutdownConnectionPool();
}
private void shutdownConnectionPool() {
connectionPool.shutdown();
try {
if (!connectionPool.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
connectionPool.shutdownNow();
}
} catch (InterruptedException e) {
connectionPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* Gets the executor service managing client connections.
*
* @return the executor service
*/
public static ExecutorService getExecutorService() {
return connectionPool;
}
/**
* Handles individual client connections in separate threads.
*/
private static class ClientHandler implements Runnable {
private final Socket clientSocket;
private final String clientInfo;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
this.clientInfo = String.format("%s:%d",
socket.getInetAddress().getHostAddress(),
socket.getPort());
}
@Override
public void run() {
LOGGER.info(() -> "New connection established with " + clientInfo);
try (Socket socket = clientSocket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
handleClientCommunication(in, out);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error handling client " + clientInfo, e);
} finally {
LOGGER.info(() -> "Connection closed with " + clientInfo);
}
}
private void handleClientCommunication(BufferedReader in, PrintWriter out) throws IOException {
String input;
while ((input = in.readLine()) != null) {
try {
String response = InputCommandRouter.processInput(ConnType.UNICAST, input);
if (response != null) {
out.println(response);
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error processing message from " + clientInfo, e);
}
}
}
}
}

View File

@@ -0,0 +1,222 @@
package client.structs;
import java.io.*;
import java.net.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.Client;
/**
* Network connection manager for the emergency communication client.
* Implements thread-safe singleton pattern and manages all network resources.
*
* Features:
* - Connection retry mechanism
* - Multiple communication channels (Unicast, Multicast, Broadcast)
* - Automatic resource cleanup
* - Connection state monitoring
*
* @author 0x1eo
* @since 2024-12-13
*/
public class NetworkManager implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(NetworkManager.class.getName());
private static volatile NetworkManager instance;
private static final Object LOCK = new Object();
private static final int MAX_CONNECTION_RETRIES = 3;
private static final int RETRY_DELAY_MS = 2000;
private static final int SOCKET_TIMEOUT_MS = 5000;
private final AtomicBoolean isAuthenticated = new AtomicBoolean(false);
private volatile String username;
private final SocketContainer sockets;
private static final int PORT_RANGE_START = 7501;
private static final int PORT_RANGE_END = 7600;
private static final int MAX_PORT_ATTEMPTS = 10;
private NetworkManager() {
this.sockets = new SocketContainer();
}
public static NetworkManager getInstance() {
NetworkManager result = instance;
if (result == null) {
synchronized (LOCK) {
result = instance;
if (result == null) {
instance = result = new NetworkManager();
}
}
}
return result;
}
/**
* Initializes primary connection with retry mechanism.
*
* @throws NetworkInitializationException if connection fails after retries
*/
public void initializePrimaryConnection() {
for (int attempt = 1; attempt <= MAX_CONNECTION_RETRIES; attempt++) {
try {
establishPrimaryConnection();
LOGGER.info("Primary connection initialized successfully on attempt " + attempt);
return;
} catch (IOException e) {
handleConnectionRetry(attempt, e);
}
}
throw new NetworkInitializationException("Failed to initialize after " + MAX_CONNECTION_RETRIES + " attempts", null);
}
private void establishPrimaryConnection() throws IOException {
Socket unicastSocket = new Socket(Client.SERVER_ADDRESS, Client.SERVER_PORT);
unicastSocket.setSoTimeout(SOCKET_TIMEOUT_MS);
sockets.unicastSocket = unicastSocket;
sockets.unicastIn = new BufferedReader(
new InputStreamReader(unicastSocket.getInputStream())
);
sockets.unicastOut = new PrintWriter(
unicastSocket.getOutputStream(),
true
);
}
private void handleConnectionRetry(int attempt, IOException e) {
LOGGER.log(Level.WARNING,
String.format("Connection attempt %d/%d failed", attempt, MAX_CONNECTION_RETRIES),
e
);
if (attempt < MAX_CONNECTION_RETRIES) {
try {
Thread.sleep(RETRY_DELAY_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new NetworkInitializationException("Connection retry interrupted", ie);
}
}
}
/**
* Initializes authenticated connections for multicast and broadcast.
*
* @throws NetworkInitializationException if initialization fails
*/
public void initializeAuthenticatedConnections() {
for (int port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
try {
setupAuthenticatedSockets(port);
isAuthenticated.set(true);
LOGGER.info(String.format(
"Authenticated connections initialized for user %s on port %d",
username, port
));
return;
} catch (IOException e) {
if (port >= PORT_RANGE_END) {
isAuthenticated.set(false);
throw new NetworkInitializationException(
"Failed to find available ports in range " +
PORT_RANGE_START + "-" + PORT_RANGE_END,
e
);
}
LOGGER.warning(String.format(
"Port %d in use, trying next port", port
));
}
}
}
private void setupAuthenticatedSockets(int basePort) throws IOException {
// Server socket uses the base port
sockets.serverSocket = new ServerSocket(basePort);
// Multicast socket uses the multicast port
sockets.multicastSocket = new MulticastSocket(Client.MULTICAST_PORT);
// Broadcast socket uses base port + 1 to avoid conflict
sockets.broadcastSocket = createBroadcastSocket(basePort + 1);
// Update the client port in the Client class
Client.CLIENT_PORT = basePort;
}
private DatagramSocket createBroadcastSocket(int port) throws IOException {
DatagramSocket socket = new DatagramSocket(
port,
InetAddress.getByName(Client.BROADCAST_ADDRESS)
);
socket.setBroadcast(true);
return socket;
}
@Override
public void close() {
sockets.closeAll();
isAuthenticated.set(false);
LOGGER.info("Network manager closed successfully");
}
// Thread-safe getters
public boolean isAuthenticated() { return isAuthenticated.get(); }
public String getUsername() { return username; }
public Socket getUnicastSocket() { return sockets.unicastSocket; }
public BufferedReader getUnicastIn() { return sockets.unicastIn; }
public PrintWriter getUnicastOut() { return sockets.unicastOut; }
public ServerSocket getServerSocket() { return sockets.serverSocket; }
public MulticastSocket getMulticastSocket() { return sockets.multicastSocket; }
public DatagramSocket getBroadcastSocket() { return sockets.broadcastSocket; }
// Thread-safe setter
public void setUsername(String username) {
this.username = username;
LOGGER.info("Username set to: " + username);
}
/**
* Thread-safe container for network resources.
*/
private static class SocketContainer {
private volatile Socket unicastSocket;
private volatile BufferedReader unicastIn;
private volatile PrintWriter unicastOut;
private volatile ServerSocket serverSocket;
private volatile MulticastSocket multicastSocket;
private volatile DatagramSocket broadcastSocket;
private void closeAll() {
closeQuietly(broadcastSocket, "BroadcastSocket");
closeQuietly(multicastSocket, "MulticastSocket");
closeQuietly(serverSocket, "ServerSocket");
closeQuietly(unicastSocket, "UnicastSocket");
closeQuietly(unicastOut, "UnicastWriter");
closeQuietly(unicastIn, "UnicastReader");
}
private void closeQuietly(AutoCloseable resource, String resourceName) {
if (resource != null) {
try {
resource.close();
LOGGER.fine(resourceName + " closed successfully");
} catch (Exception e) {
LOGGER.warning(resourceName + " close failed: " + e.getMessage());
}
}
}
}
/**
* Custom exception for network initialization failures.
*/
public static class NetworkInitializationException extends RuntimeException {
public NetworkInitializationException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -0,0 +1,18 @@
package client.structs;
public class TerminalMessageHandler {
public static void displayMessage(String from, String to, String content, String date) {
System.out.printf("[%s] %s -> %s: %s%n", date, from, to, content);
}
public static void displayRequest(String from, String to, String content, String date, String accepter) {
String accepterStatus = accepter.isEmpty() ? "PENDING" : "ACCEPTED by " + accepter;
System.out.printf("[%s] REQUEST from %s to %s: %s (%s)%n", date, from, to, content, accepterStatus);
}
public static boolean promptRequestAcceptance(String from, String to, String content) {
System.out.printf("Accept request from %s to %s: %s%n", from, to, content);
System.out.print("Accept? (y/n): ");
return System.console().readLine().trim().toLowerCase().startsWith("y");
}
}

View File

@@ -0,0 +1,162 @@
package client.utils;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import client.structs.NetworkManager;
import client.structs.TerminalMessageHandler;
/**
* Handles chat message operations including creation, receiving, and processing of messages and requests.
*
* @author 0x1eo
* @since 2024-12-12
*/
public class ChatMessageHandler {
private static final Logger LOGGER = Logger.getLogger(ChatMessageHandler.class.getName());
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
private static final NetworkManager networkManager = NetworkManager.getInstance();
private ChatMessageHandler() {} // Prevent instantiation
/**
* Message types supported by the chat system.
*/
public enum MessageType {
MESSAGE("message"),
REQUEST("request"),
JOIN_GROUP("joinGroup");
private final String command;
MessageType(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
}
/**
* Creates a basic message structure with common fields.
*/
private static JSONObject createBaseMessage(MessageType type, String destination, String content) {
JSONObject json = new JSONObject();
json.put("command", type.getCommand());
json.put("from", networkManager.getUsername());
json.put("to", destination);
json.put("content", content);
json.put("date", LocalDateTime.now().format(DATE_FORMATTER));
return json;
}
public static JSONObject createMessage(String destination, String content) {
return createBaseMessage(MessageType.MESSAGE, destination, content);
}
public static JSONObject createRequest(String destination, String content) {
JSONObject json = createBaseMessage(MessageType.REQUEST, destination, content);
json.put("accepter", "");
return json;
}
/**
* Validates if a JSON object contains all required fields.
*/
private static boolean validateMessageFields(JSONObject json, String... requiredFields) {
for (String field : requiredFields) {
if (!json.has(field)) {
LOGGER.warning("Missing required field: " + field);
return false;
}
}
return true;
}
public static void receiveMessage(JSONObject json) {
if (!validateMessageFields(json, "from", "to", "content", "date")) {
return;
}
TerminalMessageHandler.displayMessage(
json.getString("from"),
json.getString("to"),
json.getString("content"),
json.getString("date")
);
}
public static void receiveRequest(JSONObject json) {
if (!validateMessageFields(json, "from", "to", "content", "date", "accepter")) {
return;
}
TerminalMessageHandler.displayRequest(
json.getString("from"),
json.getString("to"),
json.getString("content"),
json.getString("date"),
json.getString("accepter")
);
}
public static String handleAnswerRequest(JSONObject json) {
if (!validateMessageFields(json, "from", "to", "content")) {
return null;
}
boolean accepted = TerminalMessageHandler.promptRequestAcceptance(
json.getString("from"),
json.getString("to"),
json.getString("content")
);
return new JSONObject().put("response", accepted ? "YES" : "NO").toString();
}
public static void processEvents(JSONObject json) {
if (!json.has("events")) {
LOGGER.warning("No events field in JSON");
return;
}
JSONArray events = json.getJSONArray("events");
events.forEach(event -> {
JSONObject eventObj = (JSONObject) event;
if (!eventObj.has("command")) {
LOGGER.warning("Event missing command field");
return;
}
String command = eventObj.getString("command");
try {
switch (MessageType.valueOf(command.toUpperCase())) {
case MESSAGE -> receiveMessage(eventObj);
case REQUEST -> receiveRequest(eventObj);
default -> LOGGER.warning("Unknown command: " + command);
}
} catch (IllegalArgumentException e) {
LOGGER.warning("Invalid command type: " + command);
}
});
}
public static void announceJoinGroup(String ip) {
try {
JSONObject json = new JSONObject()
.put("command", MessageType.JOIN_GROUP.getCommand())
.put("group", ip)
.put("username", networkManager.getUsername());
networkManager.getUnicastOut().println(json);
} catch (JSONException e) {
LOGGER.severe("Failed to announce group join: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,157 @@
package client.utils;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONObject;
import shared.enums.ConnType;
import client.structs.NetworkManager;
import shared.enums.Hierarchy;
/**
* Client-side command router that processes and formats commands according to server protocol.
*
* @author 0x1eo
* @since 2024-12-13 16:41:03
*/
public class InputCommandRouter {
private static final Logger LOGGER = Logger.getLogger(InputCommandRouter.class.getName());
private static final NetworkManager networkManager = NetworkManager.getInstance();
/**
* Processes input and formats it according to server protocol.
*
* @param connType The type of connection (UNICAST, MULTICAST, BROADCAST)
* @param input The user's input string
* @return Formatted JSON string or null if no response needed
*/
public static String processInput(ConnType connType, String input) {
if (input == null || input.trim().isEmpty()) {
return null;
}
String trimmedInput = input.trim();
// Handle client-side commands
if (trimmedInput.startsWith("{command:")) {
return handleCommand(connType, trimmedInput);
}
// Handle messages and requests
return handleMessage(connType, trimmedInput);
}
private static String handleCommand(ConnType connType, String command) {
if (connType != ConnType.UNICAST) {
return null;
}
// Split by : but keep quotes intact
String[] parts = command.substring(9, command.length() - 1).split(":");
String cmd = parts[0].toLowerCase();
switch (cmd) {
case "help":
displayHelp();
return null;
case "register":
if (parts.length != 4) {
System.out.println("Invalid register format. Use: {command:register:username:name:password}");
return null;
}
JSONObject registerJson = new JSONObject()
.put("command", "register")
.put("username", parts[1])
.put("name", parts[2])
.put("password", parts[3])
.put("role", Hierarchy.LOW.name());
// Debug log
LOGGER.info("Sending registration JSON: " + registerJson.toString());
return registerJson.toString();
default:
LOGGER.warning("Unknown command: " + command);
System.out.println("Unknown command. Type {command:help} for available commands.");
return null;
}
}
private static void displayHelp() {
System.out.println("\nAvailable Commands:");
System.out.println("------------------");
System.out.println("{command:register:username:name:password} - Register with the server");
System.out.println("Example: {command:register:leo:Leandro:0808wq21}");
System.out.println("{command:help} - Display this help message");
System.out.println("\nMessage Formats:");
System.out.println("---------------");
System.out.println("Regular message: text");
System.out.println("Direct message: @username: message");
System.out.println("Group message: #groupname: message");
System.out.println("Request: !request @username: content");
System.out.println();
}
private static String createRegistrationJson() {
JSONObject json = new JSONObject()
.put("command", "register")
.put("username", networkManager.getUsername())
.put("name", networkManager.getUsername()) // Using username as name for now
.put("password", "default") // Should be handled properly in production
.put("role", Hierarchy.LOW.name()); // Using LOW as the default hierarchy level
return json.toString();
}
private static String createLoginJson() {
JSONObject json = new JSONObject()
.put("command", "login")
.put("username", networkManager.getUsername())
.put("password", "default"); // Should be handled properly in production
return json.toString();
}
private static String handleMessage(ConnType connType, String input) {
try {
JSONObject messageJson;
if (input.startsWith("!request")) {
// Handle request
String[] parts = input.substring(9).split(":", 2);
if (parts.length != 2 || !parts[0].startsWith("@")) {
System.out.println("Invalid request format. Use: !request @username: content");
return null;
}
String destination = parts[0].substring(1).trim();
messageJson = ChatMessageHandler.createRequest(destination, parts[1].trim());
} else if (input.startsWith("@")) {
// Handle direct message
String[] parts = input.substring(1).split(":", 2);
if (parts.length != 2) {
System.out.println("Invalid direct message format. Use: @username: message");
return null;
}
messageJson = ChatMessageHandler.createMessage(parts[0].trim(), parts[1].trim());
} else if (input.startsWith("#")) {
// Handle group message
String[] parts = input.substring(1).split(":", 2);
if (parts.length != 2) {
System.out.println("Invalid group message format. Use: #groupname: message");
return null;
}
String groupName = parts[0].trim();
ChatMessageHandler.announceJoinGroup(groupName); // Join group first
messageJson = ChatMessageHandler.createMessage(groupName, parts[1].trim());
} else {
// Handle broadcast message
messageJson = ChatMessageHandler.createMessage("", input);
}
return messageJson.toString();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to process message: " + e.getMessage());
return null;
}
}
}

View File

@@ -1,94 +0,0 @@
package sd;
import java.io.IOException;
import sd.config.SimulationConfig;
import sd.engine.SimulationEngine;
/**
* Main entry point for the traffic simulation.
* * This class is responsible for loading the simulation configuration,
* initializing the {@link SimulationEngine}, and starting the simulation run.
* It also prints initial configuration details and final execution time.
*/
public class Entry {
/**
* The default path to the simulation configuration file.
* This is used if no command-line arguments are provided.
*/
private static final String DEFAULT_CONFIG_FILE = "src/main/resources/simulation.properties";
/**
* The main method to start the simulation.
* * @param args Command-line arguments. If provided, args[0] is expected
* to be the path to a custom configuration file.
*/
public static void main(String[] args) {
System.out.println("=".repeat(60));
System.out.println("TRAFFIC SIMULATION - DISCRETE EVENT SIMULATOR");
System.out.println("=".repeat(60));
try {
// 1. Load configuration
String configFile = args.length > 0 ? args[0] : DEFAULT_CONFIG_FILE;
System.out.println("Loading configuration from: " + configFile);
SimulationConfig config = new SimulationConfig(configFile);
// 2. Display configuration
displayConfiguration(config);
// 3. Create and initialize simulation engine
SimulationEngine engine = new SimulationEngine(config);
engine.initialize();
System.out.println("\n" + "=".repeat(60));
// 4. Run simulation
long startTime = System.currentTimeMillis();
engine.run();
long endTime = System.currentTimeMillis();
// 5. Display execution time
double executionTime = (endTime - startTime) / 1000.0;
System.out.println("\nExecution time: " + String.format("%.2f", executionTime) + " seconds");
System.out.println("=".repeat(60));
} catch (IOException e) {
System.err.println("Error loading configuration: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("Error during simulation: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Displays the main configuration parameters to the console.
* This provides a summary of the simulation settings before it starts.
*
* @param config The {@link SimulationConfig} object containing the loaded settings.
*/
private static void displayConfiguration(SimulationConfig config) {
System.out.println("\nSIMULATION CONFIGURATION:");
System.out.println(" Duration: " + config.getSimulationDuration() + " seconds");
System.out.println(" Arrival Model: " + config.getArrivalModel());
if ("POISSON".equalsIgnoreCase(config.getArrivalModel())) {
System.out.println(" Arrival Rate (λ): " + config.getArrivalRate() + " vehicles/second");
} else {
System.out.println(" Fixed Interval: " + config.getFixedArrivalInterval() + " seconds");
}
System.out.println(" Statistics Update Interval: " + config.getStatisticsUpdateInterval() + " seconds");
System.out.println("\nVEHICLE TYPES:");
System.out.println(" Bike: " + (config.getBikeVehicleProbability() * 100) + "% " +
"(crossing time: " + config.getBikeVehicleCrossingTime() + "s)");
System.out.println(" Light: " + (config.getLightVehicleProbability() * 100) + "% " +
"(crossing time: " + config.getLightVehicleCrossingTime() + "s)");
System.out.println(" Heavy: " + (config.getHeavyVehicleProbability() * 100) + "% " +
"(crossing time: " + config.getHeavyVehicleCrossingTime() + "s)");
}
}

View File

@@ -1,501 +0,0 @@
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;
}
}
}

View File

@@ -1,260 +0,0 @@
package sd.config;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* Class to load and manage simulation configurations.
* Configurations are read from a .properties file. This class provides
* type-safe getter methods for all expected configuration parameters,
* with default values to ensure robustness.
*/
public class SimulationConfig {
/**
* Holds all properties loaded from the file.
*/
private final Properties properties;
/**
* Constructs a new SimulationConfig object by loading properties
* from the specified file path.
*
* @param filePath The path to the .properties file (e.g., "src/main/resources/simulation.properties").
* @throws IOException If the file cannot be found or read.
*/
public SimulationConfig(String filePath) throws IOException {
properties = new Properties();
/**Tenta carregar diretamente a partir do sistema de ficheiros, se o ficheiro não existir
* (por exemplo quando executado a partir do classpath/jar),
* faz fallback para carregar a partir do classpath usando o ClassLoader.
*/
IOException lastException = null; //FIXME: melhorar esta parte para reportar erros de forma mais clara
try {
try (InputStream input = new FileInputStream(filePath)) {
properties.load(input);
return; // carregado com sucesso a partir do caminho fornecido
}
} catch (IOException e) {
lastException = e;
//tenta carregar a partir do classpath sem prefixos comuns
String resourcePath = filePath;
//Remove prefixos que apontam para src/main/resources quando presentes
resourcePath = resourcePath.replace("src/main/resources/", "").replace("src\\main\\resources\\", "");
//Remove prefixo classpath: se fornecido
if (resourcePath.startsWith("classpath:")) {
resourcePath = resourcePath.substring("classpath:".length());
if (resourcePath.startsWith("/")) resourcePath = resourcePath.substring(1);
}
InputStream resourceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
if (resourceStream == null) {
//como último recurso, tentar com um leading slash
resourceStream = SimulationConfig.class.getResourceAsStream('/' + resourcePath);
}
if (resourceStream != null) {
try (InputStream input = resourceStream) {
properties.load(input);
return;
}
}
}
if (lastException != null) throw lastException;
}
// --- Network configurations ---
/**
* Gets the host address for a specific intersection.
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @return The host (e.g., "localhost").
*/
public String getIntersectionHost(String intersectionId) {
return properties.getProperty("intersection." + intersectionId + ".host", "localhost");
}
/**
* Gets the port number for a specific intersection.
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @return The port number.
*/
public int getIntersectionPort(String intersectionId) {
return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0"));
}
/**
* Gets the host address for the dashboard server.
* @return The dashboard host.
*/
public String getDashboardHost() {
return properties.getProperty("dashboard.host", "localhost");
}
/**
* Gets the port number for the dashboard server.
* @return The dashboard port.
*/
public int getDashboardPort() {
return Integer.parseInt(properties.getProperty("dashboard.port", "9000"));
}
/**
* Gets the host address for the exit node.
* @return The exit node host.
*/
public String getExitHost() {
return properties.getProperty("exit.host", "localhost");
}
/**
* Gets the port number for the exit node.
* @return The exit node port.
*/
public int getExitPort() {
return Integer.parseInt(properties.getProperty("exit.port", "9001"));
}
// --- Simulation configurations ---
/**
* Gets the total duration of the simulation in virtual seconds.
* @return The simulation duration.
*/
public double getSimulationDuration() {
return Double.parseDouble(properties.getProperty("simulation.duration", "3600.0"));
}
/**
* Gets the vehicle arrival model ("POISSON" or "FIXED").
* @return The arrival model as a string.
*/
public String getArrivalModel() {
return properties.getProperty("simulation.arrival.model", "POISSON");
}
/**
* Gets the average arrival rate (lambda) for the POISSON model.
* This represents the average number of vehicles arriving per second.
* @return The arrival rate.
*/
public double getArrivalRate() {
return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5"));
}
/**
* Gets the fixed time interval between vehicle arrivals for the FIXED model.
* @return The fixed interval in seconds.
*/
public double getFixedArrivalInterval() {
return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0"));
}
// --- Traffic light configurations ---
/**
* Gets the duration of the GREEN light state for a specific traffic light.
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @param direction The direction of the light (e.g., "North").
* @return The green light time in seconds.
*/
public double getTrafficLightGreenTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".green";
return Double.parseDouble(properties.getProperty(key, "30.0"));
}
/**
* Gets the duration of the RED light state for a specific traffic light.
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @param direction The direction of the light (e.g., "North").
* @return The red light time in seconds.
*/
public double getTrafficLightRedTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".red";
return Double.parseDouble(properties.getProperty(key, "30.0"));
}
// --- Vehicle configurations ---
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type LIGHT.
* @return The probability for LIGHT vehicles.
*/
public double getLightVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7"));
}
/**
* Gets the average time it takes a LIGHT vehicle to cross an intersection.
* @return The crossing time in seconds.
*/
public double getLightVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0"));
}
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type BIKE.
* @return The probability for BIKE vehicles.
*/
public double getBikeVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0"));
}
/**
* Gets the average time it takes a BIKE vehicle to cross an intersection.
* @return The crossing time in seconds.
*/
public double getBikeVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5"));
}
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type HEAVY.
* @return The probability for HEAVY vehicles.
*/
public double getHeavyVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0"));
}
/**
* Gets the average time it takes a HEAVY vehicle to cross an intersection.
* @return The crossing time in seconds.
*/
public double getHeavyVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0"));
}
// --- Statistics ---
/**
* Gets the interval (in virtual seconds) between periodic statistics updates.
* @return The statistics update interval.
*/
public double getStatisticsUpdateInterval() {
return Double.parseDouble(properties.getProperty("statistics.update.interval", "10.0"));
}
// --- Generic getters ---
/**
* Generic method to get any property as a string, with a default value.
* @param key The property key.
* @param defaultValue The value to return if the key is not found.
* @return The property value or the default.
*/
public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
/**
* Generic method to get any property as a string.
* @param key The property key.
* @return The property value, or null if not found.
*/
public String getProperty(String key) {
return properties.getProperty(key);
}
}

View File

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

View File

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

View File

@@ -1,628 +0,0 @@
package sd.engine;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import sd.config.SimulationConfig;
import sd.model.Event;
import sd.model.EventType;
import sd.model.Intersection;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.util.StatisticsCollector;
import sd.util.VehicleGenerator;
/**
* Core simulation engine using discrete event simulation (DES).
* * This class orchestrates the entire simulation. It maintains a
* {@link PriorityQueue} of {@link Event} objects, representing all
* scheduled future actions. The engine processes events in strict
* chronological order (based on their timestamp).
* * It manages the simulation's state, including:
* - The current simulation time ({@code currentTime}).
* - The collection of all {@link Intersection} objects.
* - The {@link VehicleGenerator} for creating new vehicles.
* - The {@link StatisticsCollector} for tracking metrics.
*/
public class SimulationEngine {
/**
* Holds all simulation parameters loaded from the properties file.
*/
private final SimulationConfig config;
/**
* The core of the discrete event simulation. Events are pulled from this
* queue in order of their timestamp.
*/
private final PriorityQueue<Event> eventQueue;
/**
* A map storing all intersections in the simulation, keyed by their ID (e.g., "Cr1").
*/
private final Map<String, Intersection> intersections;
/**
* Responsible for creating new vehicles according to the configured arrival model.
*/
private final VehicleGenerator vehicleGenerator;
/**
* Collects and calculates statistics throughout the simulation.
*/
private final StatisticsCollector statisticsCollector;
/**
* The current time in the simulation (in virtual seconds).
* This time advances based on the timestamp of the event being processed.
*/
private double currentTime;
/**
* A simple counter to generate unique IDs for vehicles.
*/
private int vehicleCounter;
/**
* Constructs a new SimulationEngine.
*
* @param config The {@link SimulationConfig} object containing all
* simulation parameters.
*/
public SimulationEngine(SimulationConfig config) {
this.config = config;
this.eventQueue = new PriorityQueue<>();
this.intersections = new HashMap<>();
this.vehicleGenerator = new VehicleGenerator(config);
this.statisticsCollector = new StatisticsCollector(config);
this.currentTime = 0.0;
this.vehicleCounter = 0;
}
/**
* Initializes the simulation. This involves:
* 1. Creating all {@link Intersection} and {@link TrafficLight} objects.
* 2. Configuring the routing logic between intersections.
* 3. Scheduling the initial events (first traffic light changes,
* first vehicle generation, and periodic statistics updates).
*/
public void initialize() {
System.out.println("Initializing simulation...");
setupIntersections();
setupRouting();
// Schedule initial events to "bootstrap" the simulation
scheduleTrafficLightEvents();
scheduleNextVehicleGeneration(0.0);
scheduleStatisticsUpdates();
System.out.println("Simulation initialized with " + intersections.size() + " intersections");
}
/**
* Creates all intersections defined in the configuration
* and adds their corresponding traffic lights.
*/
private void setupIntersections() {
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
// Note: "North" is commented out, so it won't be created.
String[] directions = {/*"North",*/ "South", "East", "West"};
for (String id : intersectionIds) {
Intersection intersection = new Intersection(id);
// Add traffic lights for each configured direction
for (String direction : directions) {
double greenTime = config.getTrafficLightGreenTime(id, direction);
double redTime = config.getTrafficLightRedTime(id, direction);
TrafficLight light = new TrafficLight(
id + "-" + direction,
direction,
greenTime,
redTime
);
intersection.addTrafficLight(light);
}
intersections.put(id, intersection);
}
}
/**
* Configures how vehicles should be routed between intersections.
* This hardcoded logic defines the "map" of the city.
* * For example, `intersections.get("Cr1").configureRoute("Cr2", "East");` means
* "at intersection Cr1, any vehicle whose *next* destination is Cr2
* should be sent to the 'East' traffic light queue."
*/
private void setupRouting() {
// Cr1 routing
intersections.get("Cr1").configureRoute("Cr2", "East");
intersections.get("Cr1").configureRoute("Cr4", "South");
// Cr2 routing
intersections.get("Cr2").configureRoute("Cr1", "West");
intersections.get("Cr2").configureRoute("Cr3", "East");
intersections.get("Cr2").configureRoute("Cr5", "South");
// Cr3 routing
intersections.get("Cr3").configureRoute("Cr2", "West");
intersections.get("Cr3").configureRoute("S", "South"); // "S" is the exit
// Cr4 routing
//intersections.get("Cr4").configureRoute("Cr1", "North");
intersections.get("Cr4").configureRoute("Cr5", "East");
// Cr5 routing
//intersections.get("Cr5").configureRoute("Cr2", "North");
//intersections.get("Cr5").configureRoute("Cr4", "West");
intersections.get("Cr5").configureRoute("S", "East"); // "S" is the exit
}
/**
* Schedules the initial {@link EventType#TRAFFIC_LIGHT_CHANGE} event
* for every traffic light in the simulation.
* A small random delay is added to "stagger" the lights, preventing
* all of them from changing at the exact same time at t=0.
*/
private void scheduleTrafficLightEvents() {
for (Intersection intersection : intersections.values()) {
for (TrafficLight light : intersection.getTrafficLights()) {
// Start with lights in RED state, schedule first GREEN change
// Stagger the start times slightly to avoid all lights changing at once
double staggerDelay = Math.random() * 1.5;
scheduleTrafficLightChange(light, intersection.getId(), staggerDelay);
}
}
}
/**
* Creates and schedules a new {@link EventType#TRAFFIC_LIGHT_CHANGE} event.
* The event is scheduled to occur at {@code currentTime + delay}.
*
* @param light The {@link TrafficLight} that will change state.
* @param intersectionId The ID of the intersection where the light is located.
* @param delay The time (in seconds) from {@code currentTime} when the change should occur.
*/
private void scheduleTrafficLightChange(TrafficLight light, String intersectionId, double delay) {
double changeTime = currentTime + delay;
Event event = new Event(changeTime, EventType.TRAFFIC_LIGHT_CHANGE, light, intersectionId);
eventQueue.offer(event);
}
/**
* Schedules the next {@link EventType#VEHICLE_GENERATION} event.
* The time of the next arrival is determined by the {@link VehicleGenerator}.
*
* @param baseTime The time from which to calculate the next arrival (usually {@code currentTime}).
*/
private void scheduleNextVehicleGeneration(double baseTime) {
// Get the absolute time for the next arrival.
double nextArrivalTime = vehicleGenerator.getNextArrivalTime(baseTime);
// Only schedule the event if it's within the simulation's total duration.
if (nextArrivalTime < config.getSimulationDuration()) {
Event event = new Event(nextArrivalTime, EventType.VEHICLE_GENERATION, null, null);
eventQueue.offer(event);
}
}
/**
* Schedules all periodic {@link EventType#STATISTICS_UPDATE} events
* for the entire duration of the simulation.
*/
private void scheduleStatisticsUpdates() {
double interval = config.getStatisticsUpdateInterval();
double duration = config.getSimulationDuration();
for (double time = interval; time < duration; time += interval) {
Event event = new Event(time, EventType.STATISTICS_UPDATE, null, null);
eventQueue.offer(event);
}
}
/**
* Runs the main simulation loop.
* The loop continues as long as there are events in the queue and
* the {@code currentTime} is less than the total simulation duration.
* * In each iteration, it:
* 1. Polls the next event from the {@link #eventQueue}.
* 2. Advances {@link #currentTime} to the event's timestamp.
* 3. Calls {@link #processEvent(Event)} to handle the event.
* * After the loop, it prints the final statistics.
*/
public void run() {
System.out.println("Starting simulation...");
double duration = config.getSimulationDuration();
while (!eventQueue.isEmpty() && currentTime < duration) {
// Get the next event in chronological order
Event event = eventQueue.poll();
// Advance simulation time to this event's time
currentTime = event.getTimestamp();
// Process the event
processEvent(event);
}
System.out.println("\nSimulation completed at t=" + String.format("%.2f", currentTime) + "s");
printFinalStatistics();
}
/**
* Main event processing logic.
* Delegates the event to the appropriate handler method based on its {@link EventType}.
*
* @param event The {@link Event} to be processed.
*/
private void processEvent(Event event) {
switch (event.getType()) {
case VEHICLE_GENERATION -> handleVehicleGeneration();
case VEHICLE_ARRIVAL -> handleVehicleArrival(event);
case TRAFFIC_LIGHT_CHANGE -> handleTrafficLightChange(event);
case CROSSING_START -> handleCrossingStart(event);
case CROSSING_END -> handleCrossingEnd(event);
case STATISTICS_UPDATE -> handleStatisticsUpdate();
default -> System.err.println("Unknown event type: " + event.getType());
}
}
/**
* Handles {@link EventType#VEHICLE_GENERATION}.
* 1. Creates a new {@link Vehicle} using the {@link #vehicleGenerator}.
* 2. Records the generation event with the {@link #statisticsCollector}.
* 3. Schedules a {@link EventType#VEHICLE_ARRIVAL} event for the vehicle
* at its first destination intersection.
* 4. Schedules the *next* {@link EventType#VEHICLE_GENERATION} event.
* (Note: This line is commented out in the original, which might be a bug,
* as it implies only one vehicle is ever generated. It should likely be active.)
*/
private void handleVehicleGeneration() {
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());
// Register with statistics collector
statisticsCollector.recordVehicleGeneration(vehicle, currentTime);
// Schedule arrival at first intersection
String firstIntersection = vehicle.getCurrentDestination();
if (firstIntersection != null && !firstIntersection.equals("S")) {
// Assume minimal travel time to first intersection (e.g., 1-3 seconds)
double arrivalTime = currentTime + 1.0 + Math.random() * 2.0;
Event arrivalEvent = new Event(arrivalTime, EventType.VEHICLE_ARRIVAL, vehicle, firstIntersection);
eventQueue.offer(arrivalEvent);
}
// Schedule next vehicle generation
// This was commented out in the original file.
// For a continuous simulation, it should be enabled:
scheduleNextVehicleGeneration(currentTime);
}
/**
* Handles {@link EventType#VEHICLE_ARRIVAL} at an intersection.
* 1. Records the arrival for statistics.
* 2. Advances the vehicle's internal route planner to its *next* destination.
* 3. If the next destination is the exit ("S") or null,
* the vehicle exits the system via {@link #handleVehicleExit(Vehicle)}.
* 4. Otherwise, the vehicle is placed in the correct queue at the
* current intersection using {@link Intersection#receiveVehicle(Vehicle)}.
* 5. Attempts to process the vehicle immediately if its light is green.
*
* @param event The arrival event, containing the {@link Vehicle} and intersection ID.
*/
private void handleVehicleArrival(Event event) {
Vehicle vehicle = (Vehicle) event.getData();
String intersectionId = event.getLocation();
Intersection intersection = intersections.get(intersectionId);
if (intersection == null) {
System.err.println("Unknown intersection: " + intersectionId);
return;
}
System.out.printf("[t=%.2f] Vehicle %s arrived at %s%n",
currentTime, vehicle.getId(), intersectionId);
// Record arrival time (used to calculate waiting time later)
statisticsCollector.recordVehicleArrival(vehicle, intersectionId, currentTime);
// Advance the vehicle's route to the *next* stop
// (it has now arrived at its *current* destination)
boolean hasNext = vehicle.advanceRoute();
if (!hasNext) {
// This was the last stop
handleVehicleExit(vehicle);
return;
}
String nextDestination = vehicle.getCurrentDestination();
if (nextDestination == null || "S".equals(nextDestination)) {
// Next stop is the exit
handleVehicleExit(vehicle);
return;
}
// Add vehicle to the appropriate traffic light queue based on its next destination
intersection.receiveVehicle(vehicle);
// Try to process the vehicle immediately if its light is already green
tryProcessVehicle(vehicle, intersection);
}
/**
* Checks if a newly arrived vehicle (or a vehicle in a queue
* that just turned green) can start crossing.
*
* @param vehicle The vehicle to process.
* @param intersection The intersection where the vehicle is.
*/
private void tryProcessVehicle(Vehicle vehicle, Intersection intersection) { //FIXME
// Find the direction (and light) this vehicle is queued at
// This logic is a bit flawed: it just finds the *first* non-empty queue
// A better approach would be to get the light from the vehicle's route
String direction = intersection.getTrafficLights().stream()
.filter(tl -> tl.getQueueSize() > 0)
.map(TrafficLight::getDirection)
.findFirst()
.orElse(null);
if (direction != null) {
TrafficLight light = intersection.getTrafficLight(direction);
// If the light is green and it's the correct one...
if (light != null && light.getState() == TrafficLightState.GREEN) {
// ...remove the vehicle from the queue (if it's at the front)
Vehicle v = light.removeVehicle();
if (v != null) {
// ...and schedule its crossing.
scheduleCrossing(v, intersection);
}
}
}
}
/**
* Schedules the crossing for a vehicle that has just been dequeued
* from a green light.
* 1. Calculates and records the vehicle's waiting time.
* 2. Schedules an immediate {@link EventType#CROSSING_START} event.
*
* @param vehicle The {@link Vehicle} that is crossing.
* @param intersection The {@link Intersection} it is crossing.
*/
private void scheduleCrossing(Vehicle vehicle, Intersection intersection) {
// Calculate time spent waiting at the red light
double waitTime = currentTime - statisticsCollector.getArrivalTime(vehicle);
vehicle.addWaitingTime(waitTime);
// Schedule crossing start event *now*
Event crossingStart = new Event(currentTime, EventType.CROSSING_START, vehicle, intersection.getId());
processEvent(crossingStart); // Process immediately
}
/**
* Handles {@link EventType#CROSSING_START}.
* 1. Determines the crossing time based on vehicle type.
* 2. Schedules a {@link EventType#CROSSING_END} event to occur
* at {@code currentTime + crossingTime}.
*
* @param event The crossing start event.
*/
private void handleCrossingStart(Event event) {
Vehicle vehicle = (Vehicle) event.getData();
String intersectionId = event.getLocation();
double crossingTime = getCrossingTime(vehicle.getType());
System.out.printf("[t=%.2f] Vehicle %s started crossing at %s (duration=%.2fs)%n",
currentTime, vehicle.getId(), intersectionId, crossingTime);
// Schedule the *end* of the crossing
double endTime = currentTime + crossingTime;
Event crossingEnd = new Event(endTime, EventType.CROSSING_END, vehicle, intersectionId);
eventQueue.offer(crossingEnd);
}
/**
* Handles {@link EventType#CROSSING_END}.
* 1. Updates intersection and vehicle statistics.
* 2. Checks the vehicle's *next* destination.
* 3. If the next destination is the exit ("S"), call {@link #handleVehicleExit(Vehicle)}.
* 4. Otherwise, schedule a {@link EventType#VEHICLE_ARRIVAL} event at the
* *next* intersection, after some travel time.
*
* @param event The crossing end event.
*/
private void handleCrossingEnd(Event event) {
Vehicle vehicle = (Vehicle) event.getData();
String intersectionId = event.getLocation();
// Update stats
Intersection intersection = intersections.get(intersectionId);
if (intersection != null) {
intersection.incrementVehiclesSent();
}
double crossingTime = getCrossingTime(vehicle.getType());
vehicle.addCrossingTime(crossingTime);
System.out.printf("[t=%.2f] Vehicle %s finished crossing at %s%n",
currentTime, vehicle.getId(), intersectionId);
// Decide what to do next
String nextDest = vehicle.getCurrentDestination();
if (nextDest != null && !nextDest.equals("S")) {
// Route to the *next* intersection
// Assume 5-10 seconds travel time between intersections
double travelTime = 5.0 + Math.random() * 5.0;
double arrivalTime = currentTime + travelTime;
Event arrivalEvent = new Event(arrivalTime, EventType.VEHICLE_ARRIVAL, vehicle, nextDest);
eventQueue.offer(arrivalEvent);
} else {
// Reached the exit
handleVehicleExit(vehicle);
}
}
/**
* Handles a vehicle exiting the simulation.
* Records final statistics for the vehicle.
*
* @param vehicle The {@link Vehicle} that has completed its route.
*/
private void handleVehicleExit(Vehicle vehicle) {
System.out.printf("[t=%.2f] Vehicle %s exited the system (wait=%.2fs, travel=%.2fs)%n",
currentTime, vehicle.getId(),
vehicle.getTotalWaitingTime(),
vehicle.getTotalTravelTime(currentTime));
// Record the exit for final statistics calculation
statisticsCollector.recordVehicleExit(vehicle, currentTime);
}
/**
* Handles {@link EventType#TRAFFIC_LIGHT_CHANGE}.
* 1. Toggles the light's state (RED to GREEN or GREEN to RED).
* 2. If the light just turned GREEN, call {@link #processGreenLight(TrafficLight, Intersection)}
* to process any waiting vehicles.
* 3. Schedules the *next* state change for this light based on its
* green/red time duration.
*
* @param event The light change event.
*/
private void handleTrafficLightChange(Event event) {
TrafficLight light = (TrafficLight) event.getData();
String intersectionId = event.getLocation();
// Toggle state
TrafficLightState newState = (light.getState() == TrafficLightState.RED)
? TrafficLightState.GREEN
: TrafficLightState.RED;
light.changeState(newState);
System.out.printf("[t=%.2f] Traffic light %s changed to %s%n",
currentTime, light.getId(), newState);
// If changed to GREEN, process waiting vehicles
if (newState == TrafficLightState.GREEN) {
Intersection intersection = intersections.get(intersectionId);
if (intersection != null) {
processGreenLight(light, intersection);
}
}
// Schedule the *next* state change for this same light
double nextChangeDelay = (newState == TrafficLightState.GREEN)
? light.getGreenTime()
: light.getRedTime();
scheduleTrafficLightChange(light, intersectionId, nextChangeDelay);
}
/**
* Processes vehicles when a light turns green.
* It loops as long as the light is green and there are vehicles in the queue,
* dequeuing one vehicle at a time and scheduling its crossing.
* * *Note*: This is a simplified model. A real simulation would
* account for the *time* it takes each vehicle to cross, processing
* one vehicle every {@code crossingTime} seconds. This implementation
* processes the entire queue "instantaneously" at the moment
* the light turns green.
*
* @param light The {@link TrafficLight} that just turned green.
* @param intersection The {@link Intersection} where the light is.
*/
private void processGreenLight(TrafficLight light, Intersection intersection) {
// While the light is green and vehicles are waiting...
while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
Vehicle vehicle = light.removeVehicle();
if (vehicle != null) {
// Dequeue one vehicle and schedule its crossing
scheduleCrossing(vehicle, intersection);
}
}
}
/**
* Handles {@link EventType#STATISTICS_UPDATE}.
* Calls the {@link StatisticsCollector} to print the current
* state of the simulation (queue sizes, averages, etc.).
*/
private void handleStatisticsUpdate() {
System.out.printf("\n=== Statistics at t=%.2f ===%n", currentTime);
statisticsCollector.printCurrentStatistics(intersections, currentTime);
System.out.println();
}
/**
* Utility method to get the configured crossing time for a given {@link VehicleType}.
*
* @param type The type of vehicle.
* @return The crossing time in seconds.
*/
private double getCrossingTime(VehicleType type) {
return switch (type) {
case BIKE -> config.getBikeVehicleCrossingTime();
case LIGHT -> config.getLightVehicleCrossingTime();
case HEAVY -> config.getHeavyVehicleCrossingTime();
default -> 2.0;
}; // Default fallback
}
/**
* Prints the final summary of statistics at the end of the simulation.
*/
private void printFinalStatistics() {
System.out.println("\n" + "=".repeat(60));
System.out.println("FINAL SIMULATION STATISTICS");
System.out.println("=".repeat(60));
statisticsCollector.printFinalStatistics(intersections, currentTime);
System.out.println("=".repeat(60));
}
// --- Public Getters ---
/**
* Gets the current simulation time.
* @return The time in virtual seconds.
*/
public double getCurrentTime() {
return currentTime;
}
/**
* Gets a map of all intersections in the simulation.
* Returns a copy to prevent external modification.
* @return A {@link Map} of intersection IDs to {@link Intersection} objects.
*/
public Map<String, Intersection> getIntersections() {
return new HashMap<>(intersections);
}
/**
* Gets the statistics collector instance.
* @return The {@link StatisticsCollector}.
*/
public StatisticsCollector getStatisticsCollector() {
return statisticsCollector;
}
}

View File

@@ -1,131 +0,0 @@
package sd.model;
import java.io.Serializable;
/**
* Represents a single event in the discrete event simulation.
* * An Event is the fundamental unit of action in the simulation. It contains:
* - A {@code timestamp} (when the event should occur).
* - A {@link EventType} (what kind of event it is).
* - Associated {@code data} (e.g., the {@link Vehicle} or {@link TrafficLight} involved).
* - An optional {@code location} (e.g., the ID of the {@link Intersection}).
* * Events are {@link Comparable}, allowing them to be sorted in a
* {@link java.util.PriorityQueue}. The primary sorting key is the
* {@code timestamp}. If timestamps are equal, {@code EventType} is used
* as a tie-breaker to ensure a consistent, deterministic order.
* * Implements {@link Serializable} so events could (in theory) be sent
* across a network in a distributed simulation.
*/
public class Event implements Comparable<Event>, Serializable {
private static final long serialVersionUID = 1L;
/**
* The simulation time (in seconds) when this event is scheduled to occur.
*/
private final double timestamp;
/**
* The type of event (e.g., VEHICLE_ARRIVAL, TRAFFIC_LIGHT_CHANGE).
*/
private final EventType type;
/**
* The data payload associated with this event.
* This could be a {@link Vehicle}, {@link TrafficLight}, or null.
*/
private final Object data;
/**
* The ID of the location where the event occurs (e.g., "Cr1").
* Can be null if the event is not location-specific (like VEHICLE_GENERATION).
*/
private final String location;
/**
* Constructs a new Event.
*
* @param timestamp The simulation time when the event occurs.
* @param type The {@link EventType} of the event.
* @param data The associated data (e.g., a Vehicle object).
* @param location The ID of the location (e.g., an Intersection ID).
*/
public Event(double timestamp, EventType type, Object data, String location) {
this.timestamp = timestamp;
this.type = type;
this.data = data;
this.location = location;
}
/**
* Convenience constructor for an Event without a specific location.
*
* @param timestamp The simulation time when the event occurs.
* @param type The {@link EventType} of the event.
* @param data The associated data (e.g., a Vehicle object).
*/
public Event(double timestamp, EventType type, Object data) {
this(timestamp, type, data, null);
}
/**
* Compares this event to another event for ordering.
* * Events are ordered primarily by {@link #timestamp} (ascending).
* If timestamps are identical, they are ordered by {@link #type} (alphabetical)
* to provide a stable, deterministic tie-breaking mechanism.
*
* @param other The other Event to compare against.
* @return A negative integer if this event comes before {@code other},
* zero if they are "equal" in sorting (though this is rare),
* or a positive integer if this event comes after {@code other}.
*/
@Override
public int compareTo(Event other) {
// Primary sort: timestamp (earlier events come first)
int cmp = Double.compare(this.timestamp, other.timestamp);
if (cmp == 0) {
// Tie-breaker: event type (ensures deterministic order)
return this.type.compareTo(other.type);
}
return cmp;
}
// --- Getters ---
/**
* @return The simulation time when the event occurs.
*/
public double getTimestamp() {
return timestamp;
}
/**
* @return The {@link EventType} of the event.
*/
public EventType getType() {
return type;
}
/**
* @return The data payload (e.g., {@link Vehicle}, {@link TrafficLight}).
* The caller must cast this to the expected type.
*/
public Object getData() {
return data;
}
/**
* @return The location ID (e.g., "Cr1"), or null if not applicable.
*/
public String getLocation() {
return location;
}
/**
* @return A string representation of the event for logging.
*/
@Override
public String toString() {
return String.format("Event{t=%.2f, type=%s, loc=%s}",
timestamp, type, location);
}
}

View File

@@ -1,45 +0,0 @@
package sd.model;
/**
* Enumeration representing all possible event types in the discrete event simulation.
* These types are used by the {@link sd.engine.SimulationEngine} to determine
* how to process a given {@link Event}.
*/
public enum EventType {
/**
* Fired when a {@link Vehicle} arrives at an {@link Intersection}.
* Data: {@link Vehicle}, Location: Intersection ID
*/
VEHICLE_ARRIVAL,
/**
* Fired when a {@link TrafficLight} is scheduled to change its state.
* Data: {@link TrafficLight}, Location: Intersection ID
*/
TRAFFIC_LIGHT_CHANGE,
/**
* Fired when a {@link Vehicle} begins to cross an {@link Intersection}.
* Data: {@link Vehicle}, Location: Intersection ID
*/
CROSSING_START,
/**
* Fired when a {@link Vehicle} finishes crossing an {@link Intersection}.
* Data: {@link Vehicle}, Location: Intersection ID
*/
CROSSING_END,
/**
* Fired when a new {@link Vehicle} should be created and added to the system.
* Data: null, Location: null
*/
VEHICLE_GENERATION,
/**
* Fired periodically to trigger the printing or sending of simulation statistics.
* Data: null, Location: null
*/
STATISTICS_UPDATE
}

View File

@@ -1,255 +0,0 @@
package sd.model;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents an intersection in the traffic simulation.
* * An Intersection acts as a central hub. It does not control logic itself,
* but it *owns* and *manages* a set of {@link TrafficLight} objects.
* * Its primary responsibilities are:
* 1. Holding a {@link TrafficLight} for each direction ("North", "East", etc.).
* 2. Maintaining a {@code routing} table that maps a vehicle's *next*
* destination (e.g., "Cr3") to a specific *direction* at *this*
* intersection (e.g., "East").
* 3. Receiving incoming vehicles and placing them in the correct
* traffic light's queue based on the routing table.
* 4. Tracking aggregate statistics for all traffic passing through it.
*/
public class Intersection {
// --- Identity and configuration ---
/**
* Unique identifier for the intersection (e.g., "Cr1", "Cr2").
*/
private final String id;
/**
* A map holding all traffic lights managed by this intersection.
* Key: Direction (String, e.g., "North", "East").
* Value: The {@link TrafficLight} object for that direction.
*/
private final Map<String, TrafficLight> trafficLights;
/**
* The routing table for this intersection.
* Key: The *next* destination ID (String, e.g., "Cr3", "S" for exit).
* Value: The *direction* (String, e.g., "East") a vehicle must take
* at *this* intersection to reach that destination.
*/
private final Map<String, String> routing;
// --- Statistics ---
/**
* Total number of vehicles that have been received by this intersection.
*/
private int totalVehiclesReceived;
/**
* Total number of vehicles that have successfully passed through (sent from) this intersection.
*/
private int totalVehiclesSent;
/**
* A running average of the waiting time for vehicles at this intersection.
* Note: This calculation might be simplified.
*/
private double averageWaitingTime;
/**
* Constructs a new Intersection with a given ID.
* Initializes empty maps for traffic lights and routing.
*
* @param id The unique identifier for this intersection (e.g., "Cr1").
*/
public Intersection(String id) {
this.id = id;
this.trafficLights = new HashMap<>();
this.routing = new HashMap<>();
this.totalVehiclesReceived = 0;
this.totalVehiclesSent = 0;
this.averageWaitingTime = 0.0;
}
/**
* Registers a new {@link TrafficLight} with this intersection.
* The light is mapped by its direction.
*
* @param trafficLight The {@link TrafficLight} object to add.
*/
public void addTrafficLight(TrafficLight trafficLight) {
trafficLights.put(trafficLight.getDirection(), trafficLight);
}
/**
* Defines a routing rule for this intersection.
* * This method builds the routing table. For example, calling
* {@code configureRoute("Cr3", "East")} means "Any vehicle
* arriving here whose next destination is 'Cr3' should be sent to
* the 'East' traffic light queue."
*
* @param nextDestination The ID of the *next* intersection or exit (e.g., "Cr3", "S").
* @param direction The direction (and thus, the traffic light)
* at *this* intersection to use (e.g., "East").
*/
public void configureRoute(String nextDestination, String direction) {
routing.put(nextDestination, direction);
}
/**
* Accepts an incoming vehicle and places it in the correct queue.
* * This method:
* 1. Increments the {@link #totalVehiclesReceived} counter.
* 2. Advances the vehicle's route (since it just arrived here)
* 3. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
* 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.
*/
public void receiveVehicle(Vehicle vehicle) {
totalVehiclesReceived++;
// Note: Route advancement is handled by SimulationEngine.handleVehicleArrival()
// before calling this method, so we don't advance here.
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);
if (direction != null && trafficLights.containsKey(direction)) {
// Found a valid route and light, add vehicle to the queue
trafficLights.get(direction).addVehicle(vehicle);
} else {
// Routing error: No rule for this destination or no light for that direction
System.err.printf(
"Routing error at %s: could not place vehicle %s (destination: %s, found direction: %s)%n",
this.id, vehicle.getId(), nextDestination, direction
);
}
}
/**
* Returns the traffic light controlling the given direction.
*
* @param direction The direction (e.g., "North").
* @return The {@link TrafficLight} object, or null if no light exists
* for that direction.
*/
public TrafficLight getTrafficLight(String direction) {
return trafficLights.get(direction);
}
/**
* Returns a list of all traffic lights managed by this intersection.
*
* @return A new {@link List} containing all {@link TrafficLight} objects.
*/
public List<TrafficLight> getTrafficLights() {
// Return a copy to prevent external modification of the internal map's values
return new ArrayList<>(trafficLights.values());
}
/**
* Returns the total number of vehicles currently queued across *all*
* traffic lights at this intersection.
*
* @return The sum of all queue sizes.
*/
public int getTotalQueueSize() {
// Uses Java Stream API:
// 1. trafficLights.values().stream() - Get a stream of TrafficLight objects
// 2. .mapToInt(TrafficLight::getQueueSize) - Convert each light to its queue size (an int)
// 3. .sum() - Sum all the integers
return trafficLights.values().stream()
.mapToInt(TrafficLight::getQueueSize)
.sum();
}
// --- Stats and getters ---
/**
* @return The unique ID of this intersection.
*/
public String getId() {
return id;
}
/**
* @return The total number of vehicles that have arrived at this intersection.
*/
public int getTotalVehiclesReceived() {
return totalVehiclesReceived;
}
/**
* @return The total number of vehicles that have successfully
* departed from this intersection.
*/
public int getTotalVehiclesSent() {
return totalVehiclesSent;
}
/**
* Increments the counter for vehicles that have successfully departed.
* This is typically called by the {@link sd.engine.SimulationEngine}
* after a vehicle finishes crossing.
*/
public void incrementVehiclesSent() {
totalVehiclesSent++;
}
/**
* @return The running average of vehicle waiting time at this intersection.
*/
public double getAverageWaitingTime() {
return averageWaitingTime;
}
/**
* Updates the running average waiting time with a new sample (a new
* vehicle's wait time).
* * Uses an incremental/weighted average formula:
* NewAvg = (OldAvg * (N-1) + NewValue) / N
* where N is the total number of vehicles sent.
*
* @param newTime The waiting time (in seconds) of the vehicle that just
* departed.
*/
public void updateAverageWaitingTime(double newTime) {
// Avoid division by zero if this is called before any vehicle is sent
if (totalVehiclesSent > 0) {
averageWaitingTime = (averageWaitingTime * (totalVehiclesSent - 1) + newTime)
/ totalVehiclesSent;
} else if (totalVehiclesSent == 1) {
// This is the first vehicle
averageWaitingTime = newTime;
}
}
/**
* @return A string summary of the intersection's current state.
*/
@Override
public String toString() {
return String.format(
"Intersection{id='%s', lights=%d, queues=%d, received=%d, sent=%d}",
id,
trafficLights.size(),
getTotalQueueSize(),
totalVehiclesReceived,
totalVehiclesSent
);
}
}

View File

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

View File

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

View File

@@ -1,315 +0,0 @@
package sd.model;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Represents a single traffic light controlling one direction at an intersection.
* * Each light maintains its own queue of {@link Vehicle} objects and
* alternates between {@link TrafficLightState#GREEN} and
* {@link TrafficLightState#RED} states.
* * This class is designed to be thread-safe for a potential concurrent
* simulation (though the current engine {@link sd.engine.SimulationEngine}
* is single-threaded). It uses a {@link ReentrantLock} to protect its
* internal state (the queue and the light state) from simultaneous access.
* * The {@link Condition} variables ({@code vehicleAdded}, {@code lightGreen})
* are included for a concurrent model where:
* - A "vehicle" thread might wait on {@code lightGreen} until the light changes.
* - A "controller" thread might wait on {@code vehicleAdded} to know when to
* process a queue.
* (Note: These Conditions are *not* used by the current discrete-event engine).
*/
public class TrafficLight {
// --- Identity and configuration ---
/**
* Unique identifier for the light (e.g., "Cr1-N").
*/
private final String id;
/**
* The direction this light controls (e.g., "North", "South").
*/
private final String direction;
/**
* The current state of the light (GREEN or RED).
*/
private TrafficLightState state;
// --- Vehicle management ---
/**
* The queue of vehicles waiting at this light.
* {@link LinkedList} is used as it's a standard {@link Queue} implementation.
*/
private final Queue<Vehicle> queue;
// --- Synchronization primitives (for thread-safety) ---
/**
* A lock to protect all mutable state ({@link #queue} and {@link #state})
* from concurrent access. Any method reading or writing these fields
* *must* acquire this lock first.
*/
private final Lock lock;
/**
* A condition variable for a potential concurrent model.
* It could be used to signal threads (e.g., a controller) that
* a new vehicle has been added to the queue.
* (Not used in the current discrete-event engine).
*/
private final Condition vehicleAdded;
/**
* A condition variable for a potential concurrent model.
* It could be used to signal waiting vehicle threads that the
* light has just turned GREEN.
* (Not used in the current discrete-event engine).
*/
private final Condition lightGreen;
// --- Timing configuration ---
/**
* The duration (in seconds) this light stays GREEN.
*/
private double greenTime;
/**
* The duration (in seconds) this light stays RED.
*/
private double redTime;
// --- Statistics ---
/**
* Counter for the total number of vehicles that have
* been dequeued (processed) by this light.
*/
private int totalVehiclesProcessed;
/**
* Constructs a new TrafficLight.
*
* @param id The unique ID (e.g., "Cr1-N").
* @param direction The direction (e.g., "North").
* @param greenTime The duration of the GREEN state in seconds.
* @param redTime The duration of the RED state in seconds.
*/
public TrafficLight(String id, String direction, double greenTime, double redTime) {
this.id = id;
this.direction = direction;
this.state = TrafficLightState.RED; // All lights start RED
this.queue = new LinkedList<>();
// Initialize synchronization objects
this.lock = new ReentrantLock();
this.vehicleAdded = lock.newCondition();
this.lightGreen = lock.newCondition();
this.greenTime = greenTime;
this.redTime = redTime;
this.totalVehiclesProcessed = 0;
}
/**
* Adds a vehicle to the *end* of the waiting queue.
* This method is thread-safe.
*
* @param vehicle The {@link Vehicle} to add.
*/
public void addVehicle(Vehicle vehicle) {
lock.lock(); // Acquire the lock
try {
queue.offer(vehicle); // Add vehicle to queue
vehicleAdded.signalAll(); // Signal (for concurrent models)
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* Removes and returns the {@link Vehicle} from the *front* of the queue.
* * This only succeeds if:
* 1. The light's state is {@link TrafficLightState#GREEN}.
* 2. The queue is not empty.
* * If these conditions are not met, it returns {@code null}.
* This method is thread-safe.
*
* @return The {@link Vehicle} at the front of the queue, or {@code null}
* if the light is RED or the queue is empty.
*/
public Vehicle removeVehicle() {
lock.lock(); // Acquire the lock
try {
if (state == TrafficLightState.GREEN && !queue.isEmpty()) {
Vehicle vehicle = queue.poll(); // Remove vehicle from queue
if (vehicle != null) {
totalVehiclesProcessed++;
}
return vehicle;
}
return null; // Light is RED or queue is empty
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* Changes the lights state (e.g., RED -> GREEN).
* If the new state is GREEN, it signals any waiting threads
* (for a potential concurrent model).
* This method is thread-safe.
*
* @param newState The {@link TrafficLightState} to set.
*/
public void changeState(TrafficLightState newState) {
lock.lock(); // Acquire the lock
try {
this.state = newState;
if (newState == TrafficLightState.GREEN) {
lightGreen.signalAll(); // Signal (for concurrent models)
}
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* Returns how many vehicles are currently in the queue.
* This method is thread-safe.
* * @return The size of the queue.
*/
public int getQueueSize() {
lock.lock(); // Acquire the lock
try {
return queue.size();
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* Checks whether the queue is empty.
* This method is thread-safe.
*
* @return {@code true} if the queue has no vehicles, {@code false} otherwise.
*/
public boolean isQueueEmpty() {
lock.lock(); // Acquire the lock
try {
return queue.isEmpty();
} finally {
lock.unlock(); // Always release the lock
}
}
// --- Getters & Setters ---
/**
* @return The unique ID of this light (e.g., "Cr1-N").
*/
public String getId() {
return id;
}
/**
* @return The direction this light controls (e.g., "North").
*/
public String getDirection() {
return direction;
}
/**
* Gets the current state of the light (GREEN or RED).
* This method is thread-safe.
*
* @return The current {@link TrafficLightState}.
*/
public TrafficLightState getState() {
lock.lock(); // Acquire the lock
try {
return state;
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* @return The configured GREEN light duration in seconds.
*/
public double getGreenTime() {
return greenTime;
}
/**
* Sets the GREEN light duration.
* @param greenTime The new duration in seconds.
*/
public void setGreenTime(double greenTime) {
this.greenTime = greenTime;
}
/**
* @return The configured RED light duration in seconds.
*/
public double getRedTime() {
return redTime;
}
/**
* Sets the RED light duration.
* @param redTime The new duration in seconds.
*/
public void setRedTime(double redTime) {
this.redTime = redTime;
}
/**
* @return The total number of vehicles processed (dequeued) by this light.
*/
public int getTotalVehiclesProcessed() {
// Note: This read is not locked, assuming it's okay
// for it to be "eventually consistent" for stats.
// For strict accuracy, it should also be locked.
return totalVehiclesProcessed;
}
/**
* @return The {@link Lock} object for advanced synchronization.
*/
public Lock getLock() {
return lock;
}
/**
* @return The {@link Condition} for vehicle additions.
*/
public Condition getVehicleAdded() {
return vehicleAdded;
}
/**
* @return The {@link Condition} for the light turning green.
*/
public Condition getLightGreen() {
return lightGreen;
}
/**
* @return A string summary of the light's current state.
*/
@Override
public String toString() {
return String.format(
"TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}",
id, direction, getState(), getQueueSize() // Use getters for thread-safety
);
}
}

View File

@@ -1,17 +0,0 @@
package sd.model;
/**
* Enumeration representing the two possible states of a {@link TrafficLight}.
*/
public enum TrafficLightState {
/**
* The light is GREEN, allowing vehicles to pass (be dequeued).
*/
GREEN,
/**
* The light is RED, blocking vehicles (they remain in the queue).
*/
RED
}

View File

@@ -1,218 +0,0 @@
package sd.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a single vehicle moving through the simulation.
*
* This class is a data object that holds the state of a vehicle, including:
* - Its unique ID, type, and entry time.
* - Its complete, pre-determined {@code route} (a list of intersection IDs).
* - Its current position in the route ({@code currentRouteIndex}).
* - Metrics for total time spent waiting at red lights and time spent crossing.
* * This object is passed around the simulation, primarily inside {@link Event}
* payloads and stored in {@link TrafficLight} queues.
* * Implements {@link Serializable} so it can be sent between processes
* or nodes (e.g., over a socket in a distributed version of the simulation).
*/
public class Vehicle implements Serializable {
private static final long serialVersionUID = 1L;
// --- Identity and configuration ---
/**
* Unique identifier for the vehicle (e.g., "V1", "V2").
*/
private final String id;
/**
* The type of vehicle (BIKE, LIGHT, HEAVY).
*/
private final VehicleType type;
/**
* The simulation time (in seconds) when the vehicle was generated.
*/
private final double entryTime;
/**
* The complete, ordered list of destinations (intersection IDs and the
* final exit "S"). Example: ["Cr1", "Cr3", "S"].
*/
private final List<String> route;
/**
* An index that tracks the vehicle's progress along its {@link #route}.
* {@code route.get(currentRouteIndex)} is the vehicle's *current*
* destination (i.e., the one it is traveling *towards* or *arriving at*).
*/
private int currentRouteIndex;
// --- Metrics ---
/**
* The total accumulated time (in seconds) this vehicle has spent
* waiting at red lights.
*/
private double totalWaitingTime;
/**
* The total accumulated time (in seconds) this vehicle has spent
* actively crossing intersections.
*/
private double totalCrossingTime;
/**
* Constructs a new Vehicle.
*
* @param id The unique ID for the vehicle.
* @param type The {@link VehicleType}.
* @param entryTime The simulation time when the vehicle is created.
* @param route The complete list of destination IDs (e.t., ["Cr1", "Cr2", "S"]).
*/
public Vehicle(String id, VehicleType type, double entryTime, List<String> route) {
this.id = id;
this.type = type;
this.entryTime = entryTime;
// Create a copy of the route list to ensure immutability
this.route = new ArrayList<>(route);
this.currentRouteIndex = 0; // Starts at the first destination
this.totalWaitingTime = 0.0;
this.totalCrossingTime = 0.0;
}
/**
* Advances the vehicle to the next stop in its route by
* incrementing the {@link #currentRouteIndex}.
* * This is typically called *after* a vehicle *arrives* at an intersection,
* to set its *next* destination before it is queued.
*
* @return {@code true} if there is still at least one more destination
* in the route, {@code false} if the vehicle has passed its
* final destination.
*/
public boolean advanceRoute() {
currentRouteIndex++;
return currentRouteIndex < route.size();
}
/**
* Gets the current destination (the next intersection or exit) that
* the vehicle is heading towards.
*
* @return The ID of the current destination (e.g., "Cr1"), or
* {@code null} if the route is complete.
*/
public String getCurrentDestination() {
return (currentRouteIndex < route.size()) ? route.get(currentRouteIndex) : null;
}
/**
* Checks if the vehicle has completed its entire route.
*
* @return {@code true} if the route index is at or past the end
* of the route list, {@code false} otherwise.
*/
public boolean hasReachedEnd() {
return currentRouteIndex >= route.size();
}
// --- Getters and metrics management ---
/**
* @return The vehicle's unique ID.
*/
public String getId() {
return id;
}
/**
* @return The vehicle's {@link VehicleType}.
*/
public VehicleType getType() {
return type;
}
/**
* @return The simulation time when the vehicle entered the system.
*/
public double getEntryTime() {
return entryTime;
}
/**
* @return A *copy* of the vehicle's complete route.
*/
public List<String> getRoute() {
// Return a copy to prevent external modification
return new ArrayList<>(route);
}
/**
* @return The current index pointing to the vehicle's destination in its route list.
*/
public int getCurrentRouteIndex() {
return currentRouteIndex;
}
/**
* @return The total accumulated waiting time in seconds.
*/
public double getTotalWaitingTime() {
return totalWaitingTime;
}
/**
* Adds a duration to the vehicle's total waiting time.
* This is called by the simulation engine when a vehicle
* starts crossing an intersection.
*
* @param time The duration (in seconds) to add.
*/
public void addWaitingTime(double time) {
totalWaitingTime += time;
}
/**
* @return The total accumulated crossing time in seconds.
*/
public double getTotalCrossingTime() {
return totalCrossingTime;
}
/**
* Adds a duration to the vehicle's total crossing time.
* This is called by the simulation engine when a vehicle
* finishes crossing an intersection.
*
* @param time The duration (in seconds) to add.
*/
public void addCrossingTime(double time) {
totalCrossingTime += time;
}
/**
* Calculates the vehicle's total time spent in the system so far.
* This is a "live" calculation.
*
* @param currentTime The current simulation time.
* @return The total elapsed time (in seconds) since the vehicle
* was generated ({@code currentTime - entryTime}).
*/
public double getTotalTravelTime(double currentTime) {
return currentTime - entryTime;
}
/**
* @return A string summary of the vehicle's current state.
*/
@Override
public String toString() {
return String.format(
"Vehicle{id='%s', type=%s, next='%s', route=%s}",
id, type, getCurrentDestination(), route
);
}
}

View File

@@ -1,27 +0,0 @@
package sd.model;
/**
* Enumeration representing the different types of vehicles in the simulation.
* Each type can have different properties, such as crossing time
* and generation probability, defined in {@link sd.config.SimulationConfig}.
*/
public enum VehicleType {
/**
* A bike or motorcycle.
* Typically has a short crossing time.
*/
BIKE,
/**
* A standard light vehicle, such as a car.
* This is usually the most common type.
*/
LIGHT,
/**
* A heavy vehicle, such as a truck or bus.
* Typically has a long crossing time.
*/
HEAVY
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,103 +0,0 @@
package sd.util;
import java.util.Random;
/**
* Utility class for generating random values used throughout the simulation.
* * Provides static methods for:
* - Generating exponentially distributed intervals (for Poisson processes).
* - Generating random integers and doubles in a range.
* - Making decisions based on probability.
* - Choosing random elements from an array.
* * It uses a single, static {@link Random} instance.
*/
public class RandomGenerator {
/**
* The single, shared Random instance for the entire simulation.
*/
private static final Random random = new Random();
/**
* Returns a random time interval that follows an exponential distribution.
* * This is a key component for modeling a Poisson process, where the
* *inter-arrival times* (time between events) are exponentially distributed.
* The formula used is the inverse transform sampling method:
* {@code Time = -ln(1 - U) / λ}
* where U is a uniform random number [0, 1) and λ (lambda) is the
* average arrival rate.
*
* @param lambda The average arrival rate (λ) (e.g., 0.5 vehicles per second).
* @return The time interval (in seconds) until the next arrival.
*/
public static double generateExponentialInterval(double lambda) {
// Math.log is the natural logarithm (ln)
// random.nextDouble() returns a value in [0.0, 1.0)
return Math.log(1 - random.nextDouble()) / -lambda;
}
/**
* Returns a random integer between {@code min} and {@code max}, inclusive.
*
* @param min The minimum possible value.
* @param max The maximum possible value.
* @return A random integer in the range [min, max].
*/
public static int generateRandomInt(int min, int max) {
// random.nextInt(N) returns a value from 0 to N-1
// (max - min + 1) is the total number of integers in the range
// + min offsets the range
return random.nextInt(max - min + 1) + min;
}
/**
* Returns a random double between {@code min} (inclusive) and {@code max} (exclusive).
*
* @param min The minimum possible value.
* @param max The maximum possible value.
* @return A random double in the range [min, max).
*/
public static double generateRandomDouble(double min, double max) {
return min + (max - min) * random.nextDouble();
}
/**
* Returns {@code true} with a given probability.
* * This is useful for making weighted decisions. For example,
* {@code occursWithProbability(0.3)} will return {@code true}
* approximately 30% of the time.
*
* @param probability A value between 0.0 (never) and 1.0 (always).
* @return {@code true} or {@code false}, based on the probability.
*/
public static boolean occursWithProbability(double probability) {
return random.nextDouble() < probability;
}
/**
* Picks a random element from the given array.
*
* @param <T> The generic type of the array.
* @param array The array to choose from.
* @return A randomly selected element from the array.
* @throws IllegalArgumentException if the array is null or empty.
*/
public static <T> T chooseRandom(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array cannot be null or empty.");
}
return array[random.nextInt(array.length)];
}
/**
* Sets the seed of the shared random number generator.
* This is extremely useful for debugging and testing, as it allows
* the simulation to be run multiple times with the *exact same*
* sequence of "random" events, making the results reproducible.
*
* @param seed The seed to use.
*/
public static void setSeed(long seed) {
random.setSeed(seed);
}
}

View File

@@ -1,379 +0,0 @@
package sd.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import sd.config.SimulationConfig;
import sd.model.Intersection;
import sd.model.Vehicle;
import sd.model.VehicleType;
/**
* Collects, manages, and reports statistics throughout the simulation.
* * This class acts as the central bookkeeper for simulation metrics. It tracks:
* - Overall system statistics (total vehicles, completion time, wait time).
* - Per-vehicle-type statistics (counts, average wait time by type).
* - Per-intersection statistics (arrivals, departures).
* * It also maintains "in-flight" data, such as the arrival time of a
* vehicle at its *current* intersection, which is necessary to
* calculate waiting time when the vehicle later departs.
*/
public class StatisticsCollector {
// --- Vehicle tracking (for in-flight vehicles) ---
/**
* Tracks the simulation time when a vehicle arrives at its *current* intersection.
* This is used later to calculate waiting time (Depart_Time - Arrive_Time).
* Key: Vehicle ID (String)
* Value: Arrival Time (Double)
*/
private final Map<String, Double> vehicleArrivalTimes;
/**
* Tracks the sequence of intersections a vehicle has visited.
* Key: Vehicle ID (String)
* Value: List of Intersection IDs (String)
*/
private final Map<String, List<String>> vehicleIntersectionHistory;
// --- Overall system statistics ---
/** Total number of vehicles created by the {@link VehicleGenerator}. */
private int totalVehiclesGenerated;
/** Total number of vehicles that have reached their final destination ("S"). */
private int totalVehiclesCompleted;
/** The sum of all *completed* vehicles' total travel times. Used for averaging. */
private double totalSystemTime;
/** The sum of all *completed* vehicles' total waiting times. Used for averaging. */
private double totalWaitingTime;
// --- Per-vehicle-type statistics ---
/**
* Tracks the total number of vehicles generated, broken down by type.
* Key: {@link VehicleType}
* Value: Count (Integer)
*/
private final Map<VehicleType, Integer> vehicleTypeCount;
/**
* Tracks the total waiting time, broken down by vehicle type.
* Key: {@link VehicleType}
* Value: Total Wait Time (Double)
*/
private final Map<VehicleType, Double> vehicleTypeWaitTime;
// --- Per-intersection statistics ---
/**
* A map to hold statistics objects for each intersection.
* Key: Intersection ID (String)
* Value: {@link IntersectionStats} object
*/
private final Map<String, IntersectionStats> intersectionStats;
/**
* Constructs a new StatisticsCollector.
* Initializes all maps and counters.
*
* @param config The {@link SimulationConfig} (not currently used, but
* could be for configuration-dependent stats).
*/
public StatisticsCollector(SimulationConfig config) {
this.vehicleArrivalTimes = new HashMap<>();
this.vehicleIntersectionHistory = new HashMap<>();
this.totalVehiclesGenerated = 0;
this.totalVehiclesCompleted = 0;
this.totalSystemTime = 0.0;
this.totalWaitingTime = 0.0;
this.vehicleTypeCount = new HashMap<>();
this.vehicleTypeWaitTime = new HashMap<>();
this.intersectionStats = new HashMap<>();
// Initialize vehicle type counters to 0
for (VehicleType type : VehicleType.values()) {
vehicleTypeCount.put(type, 0);
vehicleTypeWaitTime.put(type, 0.0);
}
}
/**
* Records that a new vehicle has been generated.
* This is called by the {@link sd.engine.SimulationEngine}
* during a {@code VEHICLE_GENERATION} event.
*
* @param vehicle The {@link Vehicle} that was just created.
* @param currentTime The simulation time of the event.
*/
public void recordVehicleGeneration(Vehicle vehicle, double currentTime) {
totalVehiclesGenerated++;
// Track by vehicle type
VehicleType type = vehicle.getType();
vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
// Initialize history tracking for this vehicle
vehicleIntersectionHistory.put(vehicle.getId(), new ArrayList<>());
}
/**
* Records that a vehicle has arrived at an intersection queue.
* This is called by the {@link sd.engine.SimulationEngine}
* during a {@code VEHICLE_ARRIVAL} event.
*
* @param vehicle The {@link Vehicle} that arrived.
* @param intersectionId The ID of the intersection it arrived at.
* @param currentTime The simulation time of the arrival.
*/
public void recordVehicleArrival(Vehicle vehicle, String intersectionId, double currentTime) {
// Store arrival time - this is the "start waiting" time
vehicleArrivalTimes.put(vehicle.getId(), currentTime);
// Track intersection history
List<String> history = vehicleIntersectionHistory.get(vehicle.getId());
if (history != null) {
history.add(intersectionId);
}
// Update per-intersection statistics
getOrCreateIntersectionStats(intersectionId).recordArrival();
}
/**
* Records that a vehicle has completed its route and exited the system.
* This is where final metrics for the vehicle are aggregated.
* This is called by the {@link sd.engine.SimulationEngine}
* when a vehicle reaches destination "S".
*
* @param vehicle The {@link Vehicle} that is exiting.
* @param currentTime The simulation time of the exit.
*/
public void recordVehicleExit(Vehicle vehicle, double currentTime) {
totalVehiclesCompleted++;
// Calculate and aggregate total system time
double systemTime = vehicle.getTotalTravelTime(currentTime);
totalSystemTime += systemTime;
// Aggregate waiting time
double waitTime = vehicle.getTotalWaitingTime();
totalWaitingTime += waitTime;
// Aggregate waiting time by vehicle type
VehicleType type = vehicle.getType();
vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);
// Clean up tracking maps to save memory
vehicleArrivalTimes.remove(vehicle.getId());
vehicleIntersectionHistory.remove(vehicle.getId());
}
/**
* Gets the time a vehicle arrived at its *current* intersection.
* This is used by the {@link sd.engine.SimulationEngine} to calculate
* wait time just before the vehicle crosses.
*
* @param vehicle The {@link Vehicle} to check.
* @return The arrival time, or 0.0 if not found.
*/
public double getArrivalTime(Vehicle vehicle) {
return vehicleArrivalTimes.getOrDefault(vehicle.getId(), 0.0);
}
/**
* Prints a "snapshot" of the current simulation statistics.
* This is called periodically by the {@link sd.engine.SimulationEngine}
* during a {@code STATISTICS_UPDATE} event.
*
* @param intersections A map of all intersections (to get queue data).
* @param currentTime The current simulation time.
*/
public void printCurrentStatistics(Map<String, Intersection> intersections, double currentTime) {
System.out.printf("--- Statistics at t=%.2f ---%n", currentTime);
System.out.printf("Vehicles: Generated=%d, Completed=%d, In-System=%d%n",
totalVehiclesGenerated,
totalVehiclesCompleted,
totalVehiclesGenerated - totalVehiclesCompleted);
if (totalVehiclesCompleted > 0) {
System.out.printf("Average System Time (so far): %.2fs%n", totalSystemTime / totalVehiclesCompleted);
System.out.printf("Average Waiting Time (so far): %.2fs%n", totalWaitingTime / totalVehiclesCompleted);
}
// Print per-intersection queue sizes
System.out.println("\nIntersection Queues:");
for (Map.Entry<String, Intersection> entry : intersections.entrySet()) {
String id = entry.getKey();
Intersection intersection = entry.getValue();
System.out.printf(" %s: Queue=%d, Received=%d, Sent=%d%n",
id,
intersection.getTotalQueueSize(),
intersection.getTotalVehiclesReceived(),
intersection.getTotalVehiclesSent());
}
}
/**
* Prints the final simulation summary statistics at the end of the run.
*
* @param intersections A map of all intersections.
* @param currentTime The final simulation time.
*/
public void printFinalStatistics(Map<String, Intersection> intersections, double currentTime) {
System.out.println("\n=== SIMULATION SUMMARY ===");
System.out.printf("Duration: %.2f seconds%n", currentTime);
System.out.printf("Total Vehicles Generated: %d%n", totalVehiclesGenerated);
System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesCompleted);
System.out.printf("Vehicles Still in System: %d%n", totalVehiclesGenerated - totalVehiclesCompleted);
// Overall averages
if (totalVehiclesCompleted > 0) {
System.out.printf("%nAVERAGE METRICS (for completed vehicles):%n");
System.out.printf(" System Time: %.2f seconds%n", totalSystemTime / totalVehiclesCompleted);
System.out.printf(" Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesCompleted);
System.out.printf(" Throughput: %.2f vehicles/second%n", totalVehiclesCompleted / currentTime);
}
// Vehicle type breakdown
System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
for (VehicleType type : VehicleType.values()) {
int count = vehicleTypeCount.get(type);
if (count > 0) {
double percentage = (count * 100.0) / totalVehiclesGenerated;
// Calculate avg wait *only* for this type
// This assumes all generated vehicles of this type *completed*
// A more accurate way would be to track completed vehicle types
double avgWait = vehicleTypeWaitTime.get(type) / count;
System.out.printf(" %s: %d (%.1f%%), Avg Wait: %.2fs%n",
type, count, percentage, avgWait);
}
}
// Per-intersection statistics
System.out.println("\nINTERSECTION STATISTICS:");
for (Map.Entry<String, Intersection> entry : intersections.entrySet()) {
String id = entry.getKey();
Intersection intersection = entry.getValue();
System.out.printf(" %s:%n", id);
System.out.printf(" Vehicles Received: %d%n", intersection.getTotalVehiclesReceived());
System.out.printf(" Vehicles Sent: %d%n", intersection.getTotalVehiclesSent());
System.out.printf(" Final Queue Size: %d%n", intersection.getTotalQueueSize());
// Traffic light details
intersection.getTrafficLights().forEach(light -> {
System.out.printf(" Light %s: State=%s, Queue=%d, Processed=%d%n",
light.getDirection(),
light.getState(),
light.getQueueSize(),
light.getTotalVehiclesProcessed());
});
}
// System health indicators
System.out.println("\nSYSTEM HEALTH:");
int totalQueuedVehicles = intersections.values().stream()
.mapToInt(Intersection::getTotalQueueSize)
.sum();
System.out.printf(" Total Queued Vehicles (at end): %d%n", totalQueuedVehicles);
if (totalVehiclesGenerated > 0) {
double completionRate = (totalVehiclesCompleted * 100.0) / totalVehiclesGenerated;
System.out.printf(" Completion Rate: %.1f%%%n", completionRate);
}
}
/**
* Gets or creates the statistics object for a given intersection.
* Uses {@code computeIfAbsent} for efficient, thread-safe-like instantiation.
*
* @param intersectionId The ID of the intersection.
* @return The {@link IntersectionStats} object for that ID.
*/
private IntersectionStats getOrCreateIntersectionStats(String intersectionId) {
// If 'intersectionId' is not in the map, create a new IntersectionStats()
// and put it in the map, then return it.
// Otherwise, just return the one that's already there.
return intersectionStats.computeIfAbsent(intersectionId, k -> new IntersectionStats());
}
/**
* Inner class to track per-intersection statistics.
* This is a simple data holder.
*/
private static class IntersectionStats {
private int totalArrivals;
private int totalDepartures;
public IntersectionStats() {
this.totalArrivals = 0;
this.totalDepartures = 0;
}
public void recordArrival() {
totalArrivals++;
}
public void recordDeparture() {
totalDepartures++;
}
public int getTotalArrivals() {
return totalArrivals;
}
public int getTotalDepartures() {
return totalDepartures;
}
}
// --- Public Getters for Final Statistics ---
/**
* @return Total vehicles generated during the simulation.
*/
public int getTotalVehiclesGenerated() {
return totalVehiclesGenerated;
}
/**
* @return Total vehicles that completed their route.
*/
public int getTotalVehiclesCompleted() {
return totalVehiclesCompleted;
}
/**
* @return The sum of all travel times for *completed* vehicles.
*/
public double getTotalSystemTime() {
return totalSystemTime;
}
/**
* @return The sum of all waiting times for *completed* vehicles.
*/
public double getTotalWaitingTime() {
return totalWaitingTime;
}
/**
* @return The average travel time for *completed* vehicles.
*/
public double getAverageSystemTime() {
return totalVehiclesCompleted > 0 ? totalSystemTime / totalVehiclesCompleted : 0.0;
}
/**
* @return The average waiting time for *completed* vehicles.
*/
public double getAverageWaitingTime() {
return totalVehiclesCompleted > 0 ? totalWaitingTime / totalVehiclesCompleted : 0.0;
}
}

View File

@@ -1,229 +0,0 @@
package sd.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import sd.config.SimulationConfig;
import sd.model.Vehicle;
import sd.model.VehicleType;
/**
* Generates vehicles for the simulation.
* * This class is responsible for two key tasks:
* 1. Determining *when* the next vehicle should arrive, based on the
* arrival model (POISSON or FIXED) from the {@link SimulationConfig}.
* 2. Creating a new {@link Vehicle} object with a randomly selected
* type (e.g., BIKE, LIGHT) and a randomly selected route.
* * Routes are predefined and organized by entry point (E1, E2, E3).
*/
public class VehicleGenerator {
private final SimulationConfig config;
private final String arrivalModel;
private final double arrivalRate; // Lambda (λ) for POISSON
private final double fixedInterval; // Interval for FIXED
// --- Predefined Routes ---
// These lists store all possible routes, grouped by where they start.
/** Routes starting from entry point E1. */
private final List<RouteWithProbability> e1Routes;
/** Routes starting from entry point E2. */
private final List<RouteWithProbability> e2Routes;
/** Routes starting from entry point E3. */
private final List<RouteWithProbability> e3Routes;
/**
* Constructs a new VehicleGenerator.
* It reads the necessary configuration and initializes the
* predefined routes.
*
* @param config The {@link SimulationConfig} object.
*/
public VehicleGenerator(SimulationConfig config) {
this.config = config;
// Cache configuration values for performance
this.arrivalModel = config.getArrivalModel();
this.arrivalRate = config.getArrivalRate();
this.fixedInterval = config.getFixedArrivalInterval();
// Initialize route lists
this.e1Routes = new ArrayList<>();
this.e2Routes = new ArrayList<>();
this.e3Routes = new ArrayList<>();
initializePossibleRoutes();
}
/**
* Defines all possible routes that vehicles can take, organized by
* their entry point (E1, E2, E3). Each route is given a
* probability, which determines how often it's chosen.
*/
private void initializePossibleRoutes() {
// E1 routes (Starts at Cr1)
e1Routes.add(new RouteWithProbability(
Arrays.asList("Cr1", "Cr4", "Cr5", "S"), 0.34)); // E1 -> Cr1 -> Cr4 -> Cr5 -> Exit
e1Routes.add(new RouteWithProbability(
Arrays.asList("Cr1", "Cr2", "Cr5", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr5 -> Exit
e1Routes.add(new RouteWithProbability(
Arrays.asList("Cr1", "Cr2", "Cr3", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr3 -> Exit
// E2 routes (Starts at Cr2)
e2Routes.add(new RouteWithProbability(
Arrays.asList("Cr2", "Cr5", "S"), 0.34)); // E2 -> Cr2 -> Cr5 -> Exit
e2Routes.add(new RouteWithProbability(
Arrays.asList("Cr2", "Cr3", "S"), 0.33)); // E2 -> Cr2 -> Cr3 -> Exit
e2Routes.add(new RouteWithProbability(
Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E2 -> Cr2 -> ... -> Exit
// E3 routes (Starts at Cr3)
e3Routes.add(new RouteWithProbability(
Arrays.asList("Cr3", "S"), 0.34)); // E3 -> Cr3 -> Exit
e3Routes.add(new RouteWithProbability(
Arrays.asList("Cr3", "Cr2", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> Cr2 -> Cr5 -> Exit
e3Routes.add(new RouteWithProbability(
Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> ... -> Exit
}
/**
* Calculates the *absolute* time of the next vehicle arrival
* based on the configured model.
* * @param currentTime The current simulation time, used as the base.
* @return The absolute time (e.g., {@code currentTime + interval})
* when the next vehicle should be generated.
*/
public double getNextArrivalTime(double currentTime) {
if ("POISSON".equalsIgnoreCase(arrivalModel)) {
// For a Poisson process, the time *between* arrivals
// follows an exponential distribution.
double interval = RandomGenerator.generateExponentialInterval(arrivalRate);
return currentTime + interval;
} else {
// For a Fixed model, the interval is constant.
return currentTime + fixedInterval;
}
}
/**
* Generates a new {@link Vehicle} object.
* This involves:
* 1. Selecting a random {@link VehicleType} based on probabilities.
* 2. Selecting a random route (entry point + path) based on probabilities.
*
* @param vehicleId The unique identifier for the new vehicle (e.g., "V123").
* @param entryTime The simulation time when this vehicle is being created.
* @return A new, configured {@link Vehicle} object.
*/
public Vehicle generateVehicle(String vehicleId, double entryTime) {
VehicleType type = selectVehicleType();
List<String> route = selectRandomRoute();
return new Vehicle(vehicleId, type, entryTime, route);
}
/**
* Selects a {@link VehicleType} (BIKE, LIGHT, HEAVY) based on the
* probabilities defined in the {@link SimulationConfig}.
* * Uses a standard "cumulative probability" technique:
* 1. Get a random number {@code rand} from [0, 1).
* 2. If {@code rand < P(Bike)}, return BIKE.
* 3. Else if {@code rand < P(Bike) + P(Light)}, return LIGHT.
* 4. Else, return HEAVY.
*
* @return The selected {@link VehicleType}.
*/
private VehicleType selectVehicleType() {
double bikeProbability = config.getBikeVehicleProbability();
double lightProbability = config.getLightVehicleProbability();
double heavyProbability = config.getHeavyVehicleProbability();
// Normalize probabilities in case they don't sum to exactly 1.0
double total = bikeProbability + lightProbability + heavyProbability;
if (total == 0) return VehicleType.LIGHT; // Avoid division by zero
bikeProbability /= total;
lightProbability /= total;
double rand = Math.random();
if (rand < bikeProbability) {
return VehicleType.BIKE;
} else if (rand < bikeProbability + lightProbability) {
return VehicleType.LIGHT;
} else {
return VehicleType.HEAVY;
}
}
/**
* Selects a random route for a new vehicle.
* This is a two-step process:
* 1. Randomly select an entry point (E1, E2, or E3) with equal probability.
* 2. From the chosen entry point's list of routes, select one
* based on their defined probabilities (using cumulative probability).
*
* @return A {@link List} of strings representing the chosen route (e.g., ["Cr1", "Cr4", "S"]).
*/
private List<String> selectRandomRoute() {
// Step 1: Randomly select an entry point (E1, E2, or E3)
double entryRandom = Math.random();
List<RouteWithProbability> selectedRoutes;
if (entryRandom < 0.333) {
selectedRoutes = e1Routes;
} else if (entryRandom < 0.666) {
selectedRoutes = e2Routes;
} else {
selectedRoutes = e3Routes;
}
// Step 2: Select a route from the chosen list based on cumulative probabilities
double routeRand = Math.random();
double cumulative = 0.0;
for (RouteWithProbability routeWithProb : selectedRoutes) {
cumulative += routeWithProb.probability;
if (routeRand <= cumulative) {
// Return a *copy* of the route to prevent modification
return new ArrayList<>(routeWithProb.route);
}
}
// Fallback: This should only be reached if probabilities don't sum to 1
// (due to floating point errors)
return new ArrayList<>(selectedRoutes.get(0).route);
}
/**
* @return A string providing information about the generator's configuration.
*/
public String getInfo() {
int totalRoutes = e1Routes.size() + e2Routes.size() + e3Routes.size();
return String.format(
"VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routes=%d (E1:%d, E2:%d, E3:%d)}",
arrivalModel, arrivalRate, fixedInterval, totalRoutes,
e1Routes.size(), e2Routes.size(), e3Routes.size()
);
}
/**
* A private inner "struct-like" class to hold a route (a List of strings)
* and its associated selection probability.
*/
private static class RouteWithProbability {
final List<String> route;
final double probability;
/**
* Constructs a new RouteWithProbability pair.
* @param route The list of intersection IDs.
* @param probability The probability (0.0 to 1.0) of this route
* being chosen *from its entry group*.
*/
RouteWithProbability(List<String> route, double probability) {
this.route = route;
this.probability = probability;
}
}
}

View File

@@ -0,0 +1,95 @@
package server;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import server.structs.SystemStateManager;
import server.handlers.ActiveUsersHandler;
import server.handlers.BroadcastHandler;
import server.handlers.UnicastHandler;
import server.handlers.DataPersistence;
import server.handlers.ThingHandler;
import server.handlers.MulticastHandler;
//import server.handlers.RequestsStatsThread;
/**
* The Server class represents the main server application.
* It handles direct connections, broadcasts, multicasts, events, active users, request statistics,
* data persistence, and provides methods for starting and closing the server.
*/
public class Server {
private static final Logger logger = Logger.getLogger(Server.class.getName());
private static final ExecutorService executorService = Executors.newFixedThreadPool(100);
public static final int BUFFER_SIZE = 1024;
public static final int SERVER_PORT = 7500;
public static final int USER_PORT = 7501;
public static final int MULTICAST_PORT = 7502;
public static final String BROADCAST_ADDRESS = "255.255.255.255";
/**
* The main method of the Server class.
* It loads the shared data, creates sockets, and starts various threads for server operations.
*
* @param args The command line arguments.
*/
public static void main(String[] args) {
try {
SystemStateManager.loadData();
} catch (Exception ignored) {
}
executorService.execute(() -> handleUnicast(SERVER_PORT));
try {
SystemStateManager.setMulticastSocket(new MulticastSocket(MULTICAST_PORT));
DatagramSocket broadcastSocket = new DatagramSocket(USER_PORT, InetAddress.getByName(BROADCAST_ADDRESS));
broadcastSocket.setBroadcast(true);
SystemStateManager.setBroadcastSocket(broadcastSocket);
} catch (IOException io) {
logger.severe("Error Creating Sockets! " + io.getMessage());
close();
}
executorService.execute(new BroadcastHandler());
executorService.execute(new MulticastHandler());
executorService.execute(new ThingHandler());
executorService.execute(new ActiveUsersHandler());
//executorService.execute(new RequestsStats());
executorService.execute(new DataPersistence());
}
/**
* Handles direct connections on the specified port.
*
* @param port The port number for direct connections.
*/
public static void handleUnicast(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
executorService.execute(new UnicastHandler(serverSocket.accept()));
}
} catch (Exception e) {
logger.severe("Error Handling Unicast Connection! " + e.getMessage());
close();
}
}
/**
* Closes the server by shutting down the executor service, saving the shared data, and exiting the application.
*/
public static void close() {
try {
executorService.shutdown();
SystemStateManager.saveData();
System.exit(0);
} catch (Exception ignored) {
}
}
}

View File

@@ -0,0 +1,195 @@
package server.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.Server;
import server.structs.SystemStateManager;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
import server.utils.MessageProtocolHandler;
import server.utils.UserHandler;
import shared.enums.ConnType;
/**
* Handles asynchronous request acceptance in the emergency communication system.
* Manages the workflow of request acceptance based on connection type and user hierarchy.
*
* Features:
* - Multi-mode communication support (unicast, multicast, broadcast)
* - Hierarchy-based request handling
* - Asynchronous operation
* - Request acknowledgment tracking
*
* @author 0x1eo
* @since 2024-12-13
*/
public class AcceptRequestHandler implements Runnable {
private static final Logger logger = Logger.getLogger(AcceptRequestHandler.class.getName());
private static final String MULTICAST_ADDRESS_PATTERN =
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
private final ConnType connectionType;
private final Request request;
/**
* Creates a new request acceptance handler.
*
* @param connectionType the type of network connection to use
* @param request the request to be processed
*/
public AcceptRequestHandler(ConnType connectionType, Request request) {
this.connectionType = connectionType;
this.request = request;
}
@Override
public void run() {
try {
JSONObject requestJson = createRequestJson();
handleRequestByConnectionType(requestJson);
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to send request answer", e);
} catch (JSONException e) {
logger.log(Level.SEVERE, "Failed to create request JSON", e);
}
}
private JSONObject createRequestJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("command", "requestAnswer");
json.put("from", request.getSender());
json.put("content", request.getMessage());
json.put("to", determineReceiver());
return json;
}
private String determineReceiver() {
Object receiver = request.getReceiver();
if (receiver instanceof User) {
return ((User) receiver).getUsername();
}
if (receiver instanceof String) {
String receiverStr = (String) receiver;
if ("broadcast".equals(receiverStr) || receiverStr.matches(MULTICAST_ADDRESS_PATTERN)) {
return receiverStr;
}
logger.severe("Invalid receiver string format");
throw new IllegalStateException("Invalid receiver format");
}
logger.severe("Invalid receiver type");
throw new IllegalStateException("Invalid receiver type");
}
private void handleRequestByConnectionType(JSONObject requestJson) throws IOException, JSONException {
switch (connectionType) {
case UNICAST:
handleUnicastRequest(requestJson);
break;
case MULTICAST:
handleMulticastRequest(requestJson);
break;
case BROADCAST:
handleBroadcastRequest(requestJson);
break;
default:
logger.warning("Unsupported connection type: " + connectionType);
}
}
private void handleUnicastRequest(JSONObject requestJson) throws IOException, JSONException {
User receiver = request.getReceiver();
String response = UserHandler.sendAndReceiveSomething(receiver, requestJson.toString());
if (response != null && new JSONObject(response).getString("response").equals("YES")) {
request.setTruster(receiver);
notifyUsers(receiver);
}
}
private void handleMulticastRequest(JSONObject requestJson) throws IOException, JSONException {
User group = request.getReceiver(); // Now correctly returns a User
List<User> eligibleUsers = getEligibleUsers(
new ArrayList<>(SystemStateManager.getUsersFromGroup(group.getUsername())), // Assuming you want to get users from the group name
request.getSender()
);
for (User user : eligibleUsers) {
if (tryAcceptRequest(user, requestJson)) {
sendMulticastNotification(group.toString());
break;
}
}
}
private void handleBroadcastRequest(JSONObject requestJson) throws IOException, JSONException {
List<User> eligibleUsers = getEligibleUsers(
new ArrayList<>(SystemStateManager.getUsers()),
request.getSender()
);
for (User user : eligibleUsers) {
if (tryAcceptRequest(user, requestJson)) {
sendBroadcastNotification();
break;
}
}
}
private List<User> getEligibleUsers(List<User> users, User sender) {
users.remove(sender);
users.removeIf(user -> !SystemStateManager.getOnlineUsers().contains(user));
users.removeIf(user -> !user.getHierarchy().isHigherThan(sender.getHierarchy()));
users.sort((u1, u2) -> u2.getHierarchy().getValue() - u1.getHierarchy().getValue());
return users;
}
private boolean tryAcceptRequest(User user, JSONObject requestJson) throws IOException, JSONException {
String response = UserHandler.sendAndReceiveSomething(user, requestJson.toString());
if (response != null && new JSONObject(response).getString("response").equals("YES")) {
request.setTruster(user);
return true;
}
return false;
}
private void notifyUsers(User receiver) throws IOException {
UserHandler.sendSomething(request.getSender(),
MessageProtocolHandler.notificationToJson(request).toString());
UserHandler.sendSomething(receiver,
MessageProtocolHandler.notificationToJson(request).toString());
}
private void sendMulticastNotification(String group) throws IOException {
String eventJson = MessageProtocolHandler.notificationToJson(request).toString();
DatagramPacket packet = new DatagramPacket(
eventJson.getBytes(),
eventJson.length(),
InetAddress.getByName(group),
Server.MULTICAST_PORT
);
SystemStateManager.getMulticastSocket().send(packet);
}
private void sendBroadcastNotification() throws IOException {
String eventJson = MessageProtocolHandler.notificationToJson(request).toString();
DatagramPacket packet = new DatagramPacket(
eventJson.getBytes(),
eventJson.length(),
InetAddress.getByName(Server.BROADCAST_ADDRESS),
Server.USER_PORT
);
SystemStateManager.getBroadcastSocket().send(packet);
}
}

View File

@@ -0,0 +1,61 @@
package server.handlers;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.intfaces.User;
import server.utils.UserHandler;
/**
* This class represents a thread that periodically checks the number of online users and sends a message to the user with the highest role.
*/
public class ActiveUsersHandler implements Runnable {
private static final Logger logger = Logger.getLogger(ActiveUsersHandler.class.getName());
/**
* The run method of the ActiveUsersThread class.
* This method is executed when the thread starts.
* It periodically checks the number of online users and sends a message to the user with the highest role.
*/
@Override
public void run() {
// Number of Online Users Only for the highest role
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
List<User> onlineUsers = SystemStateManager.getOnlineUsers();
logger.info("Number of Online Users: " + onlineUsers.size());
if (onlineUsers.size() == 0) {
continue;
}
User highestRoleUser = SystemStateManager.getHighestHierarchyUser(onlineUsers);
if (highestRoleUser == null) {
logger.severe("Highest Role User is null!");
continue;
}
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", "message");
jsonObject.put("from", "server");
jsonObject.put("to", highestRoleUser.getUsername());
jsonObject.put("content", "Number of Online Users: " + onlineUsers.size());
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm");
jsonObject.put("date", sdf.format(new Date()));
UserHandler.sendSomething(highestRoleUser, jsonObject.toString());
} catch (IOException | JSONException ignored) {
logger.severe("Error Sending Active Users! " + ignored.getMessage());
}
}
}
}

View File

@@ -0,0 +1,57 @@
package server.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.logging.Logger;
import server.Server;
import server.structs.SystemStateManager;
import server.utils.InputCommandRouter;
import server.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* The BroadcastThread class represents a thread that handles broadcasting messages to all connected clients.
*/
public class BroadcastHandler implements Runnable {
private static final Logger logger = Logger.getLogger(BroadcastHandler.class.getName());
/**
* Constructs a new BroadcastThread.
*/
public BroadcastHandler() {}
/**
* Runs the broadcast thread.
*/
@Override
public void run() {
try (
DatagramSocket broadcastSocket = SystemStateManager.getBroadcastSocket();
) {
while (true) {
byte[] buffer = new byte[Server.BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
broadcastSocket.receive(packet);
if (packet.getAddress().equals(InetAddress.getLocalHost())) {
continue;
}
String input = new String(packet.getData());
String output = InputCommandRouter.processInput(ConnType.BROADCAST, packet, input);
if (output == null) {
continue;
}
DatagramPacket response = new DatagramPacket(output.getBytes(), output.length(), packet.getAddress(), packet.getPort());
try {
broadcastSocket.send(response);
} catch (IOException io) {
logger.severe("Error Sending Broadcast Response: " + io.getMessage());
}
}
} catch (IOException io) {
logger.severe("Error Handling Broadcast Connection! " + io.getMessage());
}
}
}

View File

@@ -0,0 +1,30 @@
package server.handlers;
import java.util.logging.Logger;
import server.structs.SystemStateManager;
/**
* This class represents a thread responsible for persisting data at regular intervals.
*/
public class DataPersistence implements Runnable {
private static final Logger logger = Logger.getLogger(DataPersistence.class.getName());
/**
* The run method of the DataPersistenceThread.
* This method is responsible for saving data at regular intervals.
*/
@Override
public void run() {
while (true) {
try {
Thread.sleep(10000);
SystemStateManager.saveData();
logger.info("Data Saved");
} catch (Exception e) {
logger.severe("Error Saving Data! " + e.getMessage());
}
}
}
}

View File

@@ -0,0 +1,57 @@
package server.handlers;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.intfaces.Notification;
import server.structs.intfaces.User;
import server.utils.MessageProtocolHandler;
import server.utils.UserHandler;
/**
* This class represents a thread that retrieves and sends message history for a user.
*/
public class MessageHistoryHandler implements Runnable {
private static final Logger logger = Logger.getLogger(MessageHistoryHandler.class.getName());
private User user;
/**
* Constructs a new MessageHistoryThread object.
*
* @param user the user for whom the message history will be retrieved and sent
*/
public MessageHistoryHandler(User user) {
this.user = user;
}
/**
* Runs the thread, retrieving and sending the message history for the user.
*/
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
List<? extends Notification> notifications = SystemStateManager.getUserNotifications(user);
if (notifications.isEmpty()) {
return;
}
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("command", "history");
jsonObject.put("notifications", MessageProtocolHandler.notificationsToJson(notifications));
UserHandler.sendSomething(user, jsonObject.toString());
} catch (JSONException | IOException error) {
logger.severe("Error Sending Message History! " + error.getMessage());
}
}
}

View File

@@ -0,0 +1,58 @@
package server.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.logging.Logger;
import server.Server;
import server.structs.SystemStateManager;
import server.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* MulticastThread class.
* This class is responsible for handling the multicast connection.
* It receives multicast packets, processes them, and sends responses back to the clients.
*/
public class MulticastHandler implements Runnable {
private static final Logger logger = Logger.getLogger(MulticastHandler.class.getName());
/**
* MulticastThread constructor.
*/
public MulticastHandler() {}
/**
* Runs the multicast thread.
* It continuously receives multicast packets, processes them, and sends responses back to the clients.
*/
@Override
public void run() {
try (
MulticastSocket multicastSocket = SystemStateManager.getMulticastSocket()) {
while (true) {
byte[] buffer = new byte[Server.BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
multicastSocket.receive(packet);
if (packet.getAddress().equals(InetAddress.getLocalHost())) {
continue;
}
String input = new String(packet.getData());
String output = InputCommandRouter.processInput(ConnType.MULTICAST, packet, input);
if (output == null) {
continue;
}
DatagramPacket response = new DatagramPacket(output.getBytes(), output.length(), packet.getAddress(), packet.getPort());
try {
multicastSocket.send(response);
} catch (IOException io) {
logger.severe("Error sending Multicast Response: " + io.getMessage());
}
}
} catch (IOException io) {
logger.severe("Error Handling Multicast Connection! " + io.getMessage());
}
}
}

View File

@@ -0,0 +1,119 @@
package server.handlers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.List;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.Server;
import server.structs.SystemStateManager;
import server.structs.intfaces.Notification;
import server.structs.intfaces.Message;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
import server.utils.MessageProtocolHandler;
/**
* Handles the delivery of notifications to users in a server-side communication system.
*
* This class is a runnable thread responsible for continuously processing and delivering
* notifications from the system state manager to their intended recipients. It manages
* different types of notifications such as messages and requests, ensuring they are
* transmitted to the appropriate users via network sockets.
*
* Key responsibilities:
* <ul>
* <li>Periodically retrieves pending notifications from the system state manager</li>
* <li>Validates and delivers notifications to their intended users</li>
* <li>Handles socket connections and communication protocols</li>
* <li>Manages error scenarios such as closed sockets or failed JSON conversions</li>
* </ul>
*
* The handler operates in an infinite loop, sleeping briefly between notification checks
* to prevent excessive CPU usage. It supports different notification types and logs
* critical events for monitoring and debugging purposes.
*
* @author 0x1eo
* @since 2024-12-13
* @see SystemStateManager
* @see Notification
* @see Message
* @see Request
*/
public class ThingHandler implements Runnable {
private static final Logger logger = Logger.getLogger(ThingHandler.class.getName());
/**
* The run method is the entry point for the thread.
* It continuously checks for events in the shared object and delivers them to the appropriate users.
*/
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
List<? extends Notification> notifications = SystemStateManager.getNotificationsToDeliver();
logger.info("Notifications to deliver: " + notifications.size());
for (int i = 0; i < notifications.size(); i++) {
Notification notification = notifications.get(i);
Object receiver = notification.getReceiver();
if (receiver instanceof User) {
User user = ((User) receiver);
logger.info("Event to deliver to " + user.getUsername());
Socket socket = SystemStateManager.getUserSocket(user);
if (socket == null || socket.isClosed() || !socket.isConnected()) {
logger.severe("User Socket is Null or Closed");
SystemStateManager.removeNotificationDelivered(notification);
continue;
}
try (
Socket newSocket = new Socket(socket.getInetAddress(), Server.USER_PORT);
BufferedReader in = new BufferedReader(new InputStreamReader(newSocket.getInputStream()));
PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true)) {
if (notification instanceof Message) {
Message message = ((Message) notification);
JSONObject json = MessageProtocolHandler.notificationToJson(message);
if (json == null) {
logger.severe("Event to JSON returned null!");
SystemStateManager.removeNotificationDelivered(notification);
continue;
}
out.println(MessageProtocolHandler.notificationToJson(message).toString());
logger.info("Message delivered to " + user.getUsername());
} else if (notification instanceof Request) {
Request request = ((Request) notification);
JSONObject json = MessageProtocolHandler.notificationToJson(request);
if (json == null) {
logger.severe("Event to JSON returned null!");
SystemStateManager.removeNotificationDelivered(notification);
continue;
}
out.println(MessageProtocolHandler.notificationToJson(request).toString());
logger.info("Request delivered to " + user.getUsername());
}
SystemStateManager.removeNotificationDelivered(notification);
} catch (IOException io) {
SystemStateManager.removeUserSocket(user);
} catch (JSONException json) {
SystemStateManager.removeNotificationDelivered(notification);
}
} else {
logger.severe("Receiver is not a user!");
SystemStateManager.removeNotificationDelivered(notification);
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
package server.handlers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.logging.Logger;
import server.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Represents a thread that handles direct connections with clients.
*/
public class UnicastHandler implements Runnable {
private static final Logger logger = Logger.getLogger(UnicastHandler.class.getName());
private Socket socket;
/**
* Constructs a DirectThread object with the specified socket.
*
* @param socket the socket representing the client connection
*/
public UnicastHandler(Socket socket) {
this.socket = socket;
}
/**
* Runs the thread and handles the communication with the client.
*/
@Override
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);) {
try {
while (true) {
String input = in.readLine();
if (input == null) {
in.close();
out.close();
if (!socket.isClosed()) socket.close();
return;
}
String output = InputCommandRouter.processInput(ConnType.UNICAST, socket, input);
if (output == null) {
continue;
}
out.println(output);
}
} catch (IOException io) {
logger.severe("Error Handling Direct Message! " + io.getMessage());
}
} catch (IOException io) {
logger.severe("Error Handling Direct Connection! " + io.getMessage());
}
}
}

View File

@@ -0,0 +1,453 @@
package server.structs;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.stream.Collectors;
import server.structs.intfaces.*;
import shared.enums.Hierarchy;
/**
* Manages network communications and user data for the emergency chat server.
* Implements thread-safe operations for managing users, connections, and messages.
*
* @author 0x1eo
* @since 2024-12-12
*/
public class SystemStateManager {
// Maps usernames to their corresponding User objects for quick lookup
private static final Map<String, User> users = new HashMap<>();
// Maintains active socket connections for online users
private static final Map<User, Socket> userSockets = new HashMap<>();
// Stores pending notifications for each user in priority order
private static final Map<User, TreeSet<Notification>> userNotifications = new HashMap<>();
// Queue of notifications pending delivery to users
private static final List<Notification> notificationsToDeliver = new ArrayList<>();
// Maps multicast group addresses to their member users
private static final Map<String, List<User>> groups = new HashMap<>();
// Socket for handling multicast communication
private static MulticastSocket multicastSocket;
// Socket for handling broadcast messages
private static DatagramSocket broadcastSocket;
// Private constructor to prevent instantiation
private SystemStateManager() {
throw new AssertionError("Utility class - do not instantiate");
}
//#region User Management
/**
* Adds a new user to the system.
*
* @param user the user to be added
* @throws IllegalArgumentException if user is null or if username already exists
*/
public static void addUser(User user) {
validateNotNull("User", user);
synchronized (users) {
if (users.containsKey(user.getUsername())) {
throw new IllegalArgumentException("User already exists!");
}
users.put(user.getUsername(), user);
}
}
/**
* Retrieves a user by their username.
*
* @param username the username to look up
* @return the User object if found, null otherwise
* @throws IllegalArgumentException if username is null or empty
*/
public static User getUser(String username) {
validateNotEmpty("Username", username);
synchronized (users) {
return users.get(username);
}
}
/**
* Returns a list of all registered users in the system.
*
* @return new ArrayList containing all users
*/
public static List<User> getUsers() {
synchronized (users) {
return new ArrayList<>(users.values());
}
}
/**
* Gets the user with the highest hierarchy level from a list of users.
*
* @param userList list of users to check
* @return user with highest hierarchy level, or null if list is empty
*/
public static User getHighestHierarchyUser(List<User> userList) {
if (userList == null || userList.isEmpty()) {
return null;
}
return userList.stream()
.max((u1, u2) -> {
Hierarchy h1 = u1.getHierarchy();
Hierarchy h2 = u2.getHierarchy();
return Integer.compare(h1.getValue(), h2.getValue());
})
.orElse(null);
}
//#endregion
//#region Socket Management
/**
* Associates a socket connection with a user.
*
* @param user the user to associate the socket with
* @param socket the socket connection
* @throws IllegalArgumentException if either parameter is null
*/
public static void addUserSocket(User user, Socket socket) {
validateNotNull("User", user);
validateNotNull("Socket", socket);
synchronized (userSockets) {
userSockets.put(user, socket);
}
}
/**
* Retrieves the active socket connection for a user.
*
* @param user the user whose socket to retrieve
* @return the Socket object if found, null otherwise
* @throws IllegalArgumentException if user is null
*/
public static Socket getUserSocket(User user) {
validateNotNull("User", user);
synchronized (userSockets) {
return userSockets.get(user);
}
}
/**
* Returns a list of currently online users.
* A user is considered online if they have an active socket connection.
*
* @return list of users with active socket connections
*/
public static List<User> getOnlineUsers() {
synchronized (userSockets) {
return userSockets.entrySet().stream()
.filter(entry -> isSocketActive(entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}
/**
* Checks if a socket connection is active and valid.
*
* @param socket the socket to check
* @return true if socket is active and operational
*/
private static boolean isSocketActive(Socket socket) {
return socket != null &&
!socket.isClosed() &&
socket.isConnected() &&
!socket.isInputShutdown() &&
!socket.isOutputShutdown();
}
/**
* Removes a user's socket connection.
*
* @param user the user whose socket to remove
* @throws IllegalArgumentException if user is null
*/
public static void removeUserSocket(User user) {
validateNotNull("User", user);
synchronized (userSockets) {
userSockets.remove(user);
}
}
//#endregion
//#region Notification Management
/**
* Adds a notification for a specific user.
*
* @param user the target user
* @param notification the notification to add
* @throws IllegalArgumentException if either parameter is null
*/
public static void addUserNotification(User user, Notification notification) {
validateNotNull("User", user);
validateNotNull("Notification", notification);
synchronized (userNotifications) {
userNotifications.computeIfAbsent(user, k -> new TreeSet<>()).add(notification);
}
}
/**
* Retrieves all notifications for a user.
*
* @param user the user whose notifications to retrieve
* @return list of notifications, empty list if none found
* @throws IllegalArgumentException if user is null
*/
public static List<Notification> getUserNotifications(User user) {
validateNotNull("User", user);
synchronized (userNotifications) {
TreeSet<Notification> notifications = userNotifications.get(user);
return notifications != null ? new ArrayList<>(notifications) : new ArrayList<>();
}
}
/**
* Gets all pending requests in the system.
*
* @return list of all Request objects
*/
public static List<Request> getRequests() {
synchronized (userNotifications) {
return userNotifications.values().stream()
.flatMap(Collection::stream)
.filter(notification -> notification instanceof Request)
.map(notification -> (Request) notification)
.collect(Collectors.toList());
}
}
/**
* Filters a collection of requests to return only accepted ones.
*
* @param requests collection of requests to filter
* @return list of requests that have been accepted
*/
public static List<Request> getAcceptedRequests(Collection<Request> requests) {
return requests.stream()
.filter(request -> request.getTruster() != null)
.collect(Collectors.toList());
}
/**
* Adds a notification to the delivery queue.
*
* @param notification the notification to queue
* @throws IllegalArgumentException if notification is null
*/
public static void addNotificationToDeliver(Notification notification) {
validateNotNull("Notification", notification);
synchronized (notificationsToDeliver) {
notificationsToDeliver.add(notification);
}
}
/**
* Returns all notifications pending delivery.
*
* @return list of queued notifications
*/
public static List<Notification> getNotificationsToDeliver() {
synchronized (notificationsToDeliver) {
return new ArrayList<>(notificationsToDeliver);
}
}
/**
* Removes a delivered notification from the queue.
*
* @param notification the notification to remove
* @throws IllegalArgumentException if notification is null
*/
public static void removeNotificationDelivered(Notification notification) {
validateNotNull("Notification", notification);
synchronized (notificationsToDeliver) {
notificationsToDeliver.remove(notification);
}
}
//#endregion
//#region Group Management
/**
* Adds a user to a multicast group.
*
* @param group the multicast group address
* @param user the user to add
* @throws IllegalArgumentException if group is invalid or user is null
*/
public static void addUserToGroup(String group, User user) {
validateNotEmpty("Group", group);
validateNotNull("User", user);
validateMulticastAddress(group);
synchronized (groups) {
groups.computeIfAbsent(group, k -> new ArrayList<>()).add(user);
}
}
/**
* Gets all users in a specific group.
*
* @param group the group address
* @return list of users in the group
* @throws IllegalArgumentException if group is null or empty
*/
public static List<User> getUsersFromGroup(String group) {
validateNotEmpty("Group", group);
synchronized (groups) {
List<User> groupUsers = groups.get(group);
return groupUsers != null ? new ArrayList<>(groupUsers) : new ArrayList<>();
}
}
//#endregion
//#region Socket Getters/Setters
/**
* Gets the system's multicast socket.
*
* @return the MulticastSocket instance
*/
public static MulticastSocket getMulticastSocket() {
return multicastSocket;
}
/**
* Sets the system's multicast socket.
*
* @param socket the MulticastSocket to use
*/
public static void setMulticastSocket(MulticastSocket socket) {
multicastSocket = socket;
}
/**
* Gets the system's broadcast socket.
*
* @return the DatagramSocket instance
*/
public static DatagramSocket getBroadcastSocket() {
return broadcastSocket;
}
/**
* Sets the system's broadcast socket.
*
* @param socket the DatagramSocket to use
*/
public static void setBroadcastSocket(DatagramSocket socket) {
broadcastSocket = socket;
}
//#endregion
//#region Data Persistence
/**
* Gets a map of all data structures for persistence.
*
* @return map of structure names to their objects
*/
private static Map<String, Object> getDataStructures() {
Map<String, Object> structures = new HashMap<>();
structures.put("users.bin", users);
structures.put("userNotifications.bin", userNotifications);
structures.put("notificationsToDeliver.bin", notificationsToDeliver);
structures.put("groups.bin", groups);
return structures;
}
/**
* Saves all system state to persistent storage.
*
* @throws IOException if an I/O error occurs during saving
*/
public static void saveData() throws IOException {
for (Map.Entry<String, Object> entry : getDataStructures().entrySet()) {
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("./" + entry.getKey()))) {
out.writeObject(entry.getValue());
}
}
}
/**
* Loads system state from persistent storage.
*
* @throws IOException if an I/O error occurs during loading
* @throws ClassNotFoundException if a serialized class cannot be found
*/
public static void loadData() throws IOException, ClassNotFoundException {
for (Map.Entry<String, Object> entry : getDataStructures().entrySet()) {
try (ObjectInputStream in = new ObjectInputStream(
new FileInputStream(entry.getKey()))) {
Object data = in.readObject();
loadDataStructure(entry.getKey(), data);
}
}
}
/**
* Loads a specific data structure from serialized data.
*
* @param key the identifier for the data structure
* @param data the serialized data to load
*/
private static void loadDataStructure(String key, Object data) {
switch (key) {
case "users.bin":
users.putAll((Map<String, User>) data);
break;
case "userNotifications.bin":
userNotifications.putAll((Map<User, TreeSet<Notification>>) data);
break;
case "notificationsToDeliver.bin":
notificationsToDeliver.addAll((List<Notification>) data);
break;
case "groups.bin":
groups.putAll((Map<String, List<User>>) data);
break;
}
}
//#endregion
//#region Validation Helpers
/**
* Validates that an object is not null.
*
* @param field name of the field being validated
* @param value the value to check
* @throws IllegalArgumentException if value is null
*/
private static void validateNotNull(String field, Object value) {
if (value == null) {
throw new IllegalArgumentException(field + " cannot be null!");
}
}
/**
* Validates that a string is not null or empty.
*
* @param field name of the field being validated
* @param value the string to check
* @throws IllegalArgumentException if value is null or empty
*/
private static void validateNotEmpty(String field, String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(field + " cannot be null or empty!");
}
}
/**
* Validates that a string represents a valid multicast address.
*
* @param address the address to validate
* @throws IllegalArgumentException if address format is invalid
*/
private static void validateMulticastAddress(String address) {
if (!address.matches("^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
throw new IllegalArgumentException("Invalid multicast address format!");
}
}
//#endregion
}

View File

@@ -0,0 +1,28 @@
package server.structs.abstractions;
import server.structs.intfaces.Message;
import server.structs.intfaces.User;
/**
* Abstract implementation of Message interface for the emergency communication system.
* Represents the base message type that can be exchanged between users.
*
* @author 0x1eo
* @since 2024-12-13
* @see Message
* @see AbstractNotification
*/
public abstract class AbstractMessage extends AbstractNotification implements Message {
/**
* Creates a new message.
*
* @param sender the user sending the message
* @param receiver the recipient (user, broadcast, or multicast group)
* @param content the message content
* @throws IllegalArgumentException if sender or content is null
*/
protected AbstractMessage(User sender, Object receiver, String content) {
super(sender, receiver, content);
}
}

View File

@@ -0,0 +1,111 @@
package server.structs.abstractions;
import java.time.Instant;
import java.util.Objects;
import server.structs.intfaces.Notification;
import server.structs.intfaces.User;
/**
* Abstract base implementation for notifications in the emergency communication system.
* Provides common functionality for all types of communications including
* messages, requests, and alerts.
*
* Features:
* - Timestamp-based ordering
* - Sender and receiver tracking
* - Message content storage
* - Thread-safe immutable timestamp
*
* @author 0x1eo
* @since 2024-12-13 02:47:23
* @see Notification
*/
public abstract class AbstractNotification implements Notification {
protected User sender;
protected User receiver;
protected String message;
protected final Instant timestamp;
/**
* Creates a new notification.
*
* @param sender the sender's identifier object
* @param receiver the recipient's identifier object
* @param message the notification message content
* @throws IllegalArgumentException if sender, receiver or message is null
*/
protected AbstractNotification(Object sender, Object receiver, String message) {
setSender(sender);
setReceiver(receiver);
setMessage(message);
this.timestamp = Instant.now();
}
@Override
public User getSender() {
return sender;
}
@Override
public void setSender(Object sender) {
if (sender == null) {
throw new IllegalArgumentException("Sender cannot be null");
}
this.sender = (User)sender;
}
@Override
public User getReceiver() { return receiver; }
@Override
public void setReceiver(Object receiver) {
if (receiver == null) {
throw new IllegalArgumentException("Receiver cannot be null");
}
this.receiver = (User)receiver;
}
@Override
public String getMessage() { return message; }
@Override
public void setMessage(String message) {
if (message == null) {
throw new IllegalArgumentException("Message cannot be null");
}
this.message = message;
}
@Override
public Instant getTimestamp() { return timestamp; }
@Override
public void setTimestamp(Instant timestamp) {
throw new UnsupportedOperationException("Timestamp cannot be modified after creation");
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Notification)) return false;
Notification other = (Notification) obj;
return Objects.equals(sender, other.getSender()) &&
Objects.equals(receiver, other.getReceiver()) &&
Objects.equals(message, other.getMessage()) &&
Objects.equals(timestamp, other.getTimestamp());
}
@Override
public int hashCode() { return Objects.hash(sender, receiver, message, timestamp); }
@Override
public String toString() {
return String.format("Notification[from=%s, to=%s, message='%s', time=%s]",
sender,
receiver,
message,
timestamp);
}
}

View File

@@ -0,0 +1,88 @@
package server.structs.abstractions;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
/**
* Abstract base implementation for requests in the emergency communication system.
* Extends AbstractNotification to provide request-specific functionality including
* request author tracking.
*
* Features:
* - Request author (truster) tracking
* - Inherits notification base features
* - Supports emergency communication protocol
* - Immutable creation timestamp
*
* @author 0x1eo
* @since 2024-12-13
* @see Request
* @see AbstractNotification
* @see User
*/
public abstract class AbstractRequest extends AbstractNotification implements Request {
protected User truster;
/**
* Creates a new request in the emergency system.
*
* @param sender the user initiating the request
* @param receiver the intended recipient
* @param content the request content/message
* @throws IllegalArgumentException if any parameter is null, or if receiver is empty
*/
protected AbstractRequest(User sender, String receiver, String content) {
super(sender.getUsername(), receiver, content);
setTruster(sender); // Initialize truster with the sender
}
/**
* Gets the author (requesting user) of this request.
*
* @return the user who authored this request
*/
@Override
public User getTruster() {
return truster;
}
/**
* Sets the author (requesting user) of this request.
*
* @param author the user who authored this request
* @throws IllegalArgumentException if author is null
*/
@Override
public void setTruster(User author) {
if (author == null) {
throw new IllegalArgumentException("Request author cannot be null");
}
this.truster = author;
}
@Override
public String toString() {
return String.format("Request[from=%s, to=%s, content='%s', author=%s, time=%s]",
getSender(),
getReceiver(),
getMessage(),
truster.getUsername(),
getTimestamp());
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) return false;
if (!(obj instanceof Request)) return false;
Request other = (Request) obj;
return truster.equals(other.getTruster());
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + truster.hashCode();
return result;
}
}

View File

@@ -0,0 +1,125 @@
package server.structs.abstractions;
import java.util.Objects;
import server.structs.intfaces.User;
import shared.enums.Hierarchy;
/**
* Abstract base implementation of the User interface that represents
* a user in the emergency communication system with hierarchical privileges.
*
* Features:
* - Username and full name management
* - Secure password storage
* - Hierarchical role-based access
* - Natural ordering based on hierarchy
*
* @author 0x1eo
* @since 2024-12-13
* @see User
* @see Hierarchy
*/
public abstract class AbstractUser implements User {
private String username;
private String name;
private String password;
private Hierarchy hierarchy;
/**
* Creates a new user with the specified credentials and hierarchy level.
*
* @param username unique identifier for the user
* @param name full name of the user
* @param password user's authentication credential
* @param hierarchy user's position in the system hierarchy
* @throws IllegalArgumentException if any parameter is null or empty strings
*/
protected AbstractUser(String username, String name, String password, Hierarchy hierarchy) {
setUsername(username);
setName(name);
setPassword(password);
setHierarchy(hierarchy);
}
@Override
public String getUsername() {
return username;
}
@Override
public void setUsername(String username) {
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("Username cannot be null or empty");
}
this.username = username.trim();
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
this.name = name.trim();
}
@Override
public String getPassword() {
return password;
}
@Override
public void setPassword(String password) {
if (password == null || password.trim().isEmpty()) {
throw new IllegalArgumentException("Password cannot be null or empty");
}
this.password = password;
}
@Override
public Hierarchy getHierarchy() {
return hierarchy;
}
@Override
public void setHierarchy(Hierarchy hierarchy) {
if (hierarchy == null) {
throw new IllegalArgumentException("Hierarchy cannot be null");
}
this.hierarchy = hierarchy;
}
@Override
public int compareTo(User other) {
return this.hierarchy.getValue() - other.getHierarchy().getValue();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User)) return false;
User other = (User) obj;
return Objects.equals(username, other.getUsername()) &&
Objects.equals(name, other.getName()) &&
Objects.equals(password, other.getPassword()) &&
Objects.equals(hierarchy, other.getHierarchy());
}
@Override
public int hashCode() {
return Objects.hash(username, name, password, hierarchy);
}
@Override
public String toString() {
return String.format("User[username='%s', name='%s', hierarchy=%s]",
username,
name,
hierarchy.getDisplayName());
}
}

View File

@@ -0,0 +1,46 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractMessage;
import server.structs.intfaces.Message;
import server.structs.intfaces.User;
/**
* Standard message implementation for the emergency communication system.
* Provides a concrete implementation for direct, broadcast, and group messages.
*
* Features:
* - Direct user-to-user messaging
* - Broadcast messaging support
* - Multicast group communication
* - Emergency notifications
* - Timestamp-based ordering
*
* @author 0x1eo
* @since 2024-12-13 03:42:45 UTC
* @see Message
* @see AbstractMessage
* @see User
*/
public class StandardMessage extends AbstractMessage {
/**
* Creates a new message in the emergency system.
*
* @param sender the user initiating the message
* @param receiver the recipient (user, broadcast address, or multicast group)
* @param content the message content
* @throws IllegalArgumentException if sender or content is null
*/
public StandardMessage(User sender, Object receiver, String content) {
super(sender, receiver, content);
}
@Override
public String toString() {
return String.format("Message[from=%s, to=%s, content='%s', time=%s]",
getSender(),
getReceiver(),
getMessage(),
getTimestamp());
}
}

View File

@@ -0,0 +1,44 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractNotification;
import server.structs.intfaces.User;
import server.utils.UserHandler;
/**
* Standard notification implementation for the emergency communication system.
* Provides a concrete implementation of AbstractNotification for general-purpose
* system notifications.
*
* Features:
* - Direct messaging support
* - Broadcast capability
* - Multicast group messaging
* - System alerts and announcements
*
* @author 0x1eo
* @since 2024-12-13 03:41:39 UTC
* @see AbstractNotification
*/
public class StandardNotification extends AbstractNotification {
/**
* Creates a new standard notification.
*
* @param sender the sender's identifier (user, system, or service)
* @param receiver the recipient's identifier (user, broadcast, or multicast group)
* @param message the notification content
* @throws IllegalArgumentException if any parameter is null
*/
public StandardNotification(Object sender, Object receiver, String message) {
super(sender, receiver, message);
}
public String convertToUser(Object obj) {
if (obj instanceof String) {
// Use your user lookup mechanism
return UserHandler.findUser(String.valueOf(obj));
}
throw new IllegalArgumentException("Cannot convert to User: " + obj);
}
}

View File

@@ -0,0 +1,17 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractRequest;
import server.structs.intfaces.User;
/**
* Standard request implementation for the emergency communication system.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class StandardRequest extends AbstractRequest {
public StandardRequest(User sender, String receiver, String content) {
super(sender, receiver, content);
}
}

View File

@@ -0,0 +1,17 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractUser;
import shared.enums.Hierarchy;
/**
* Standard user implementation for the emergency communication system.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class StandardUser extends AbstractUser {
public StandardUser(String username, String name, String password, Hierarchy hierarchy) {
super(username, name, password, hierarchy);
}
}

View File

@@ -0,0 +1,15 @@
package server.structs.intfaces;
/**
* Represents a message in the communication system.
* Extends the base Notification interface to provide message-specific functionality.
* This interface serves as a marker for distinguishing message types from other notifications.
*
* @author 0x1eo
* @since 2024-12-12
* @see Notification
*/
public interface Message extends Notification {
// Marker interface - no additional methods required
// Implementation classes should provide message-specific functionality
}

View File

@@ -0,0 +1,86 @@
package server.structs.intfaces;
import java.io.Serializable;
import java.time.Instant;
/**
* Represents a notification in the communication system.
* Provides methods for managing notification metadata and content.
* Implements Serializable for network transmission and Comparable for ordering.
*
* @author 0x1eo
* @since 2024-12-12
*/
public interface Notification extends Serializable, Comparable<Notification> {
/**
* Gets the sender of the notification.
*
* @return the sender's identifier
*/
User getSender();
/**
* Sets the sender of the notification.
*
* @param sender the sender's identifier
* @throws IllegalArgumentException if sender is null or empty
*/
void setSender(Object sender);
/**
* Gets the receiver of the notification.
*
* @return the receiver's identifier
*/
User getReceiver();
/**
* Sets the receiver of the notification.
*
* @param receiver the receiver's identifier
* @throws IllegalArgumentException if receiver is null or empty
*/
void setReceiver(Object receiver);
/**
* Gets the content of the notification.
*
* @return the notification message content
*/
String getMessage();
/**
* Sets the content of the notification.
*
* @param message the notification message content
* @throws IllegalArgumentException if message is null
*/
void setMessage(String message);
/**
* Gets the timestamp of the notification.
*
* @return the instant when the notification was created
*/
Instant getTimestamp();
/**
* Sets the timestamp of the notification.
*
* @param timestamp the instant when the notification was created
* @throws IllegalArgumentException if timestamp is null
*/
void setTimestamp(Instant timestamp);
/**
* Provides a default natural ordering for notifications based on timestamp.
*
* @param other the notification to compare with
* @return negative if this is earlier, zero if same time, positive if later
*/
@Override
default int compareTo(Notification other) {
return this.getTimestamp().compareTo(other.getTimestamp());
}
}

View File

@@ -0,0 +1,29 @@
package server.structs.intfaces;
/**
* Represents a request in the communication system.
* Extends the Notification interface to add request-specific functionality.
* A request is a special type of notification that includes an author (requesting user).
*
* @author 0x1eo
* @since 2024-12-12
* @see Notification
* @see User
*/
public interface Request extends Notification {
/**
* Gets the author (requesting user) of this request.
*
* @return the user who authored this request
*/
User getTruster();
/**
* Sets the author (requesting user) of this request.
*
* @param author the user who authored this request
* @throws IllegalArgumentException if author is null
*/
void setTruster(User author);
}

View File

@@ -0,0 +1,101 @@
package server.structs.intfaces;
import java.io.Serializable;
import shared.enums.Hierarchy;
/**
* Represents a user in the system with their credentials and permissions.
* Implements Serializable for network transmission and Comparable for user ordering.
*
* @author 0x1eo
* @since 2024-12-12
*/
public interface User extends Serializable, Comparable<User> {
/**
* Gets the user's unique username.
*
* @return the username
*/
String getUsername();
/**
* Sets the user's username.
*
* @param username the username to set
* @throws IllegalArgumentException if username is null or empty
*/
void setUsername(String username); // Fixed method name from setUserName
/**
* Gets the user's password (hashed).
*
* @return the hashed password
*/
String getPassword();
/**
* Sets the user's password.
* Implementation should ensure the password is properly hashed before storage.
*
* @param password the password to set
* @throws IllegalArgumentException if password is null or empty
*/
void setPassword(String password);
/**
* Gets the user's display name.
*
* @return the user's full name
*/
String getName();
/**
* Sets the user's display name.
*
* @param name the name to set
* @throws IllegalArgumentException if name is null or empty
*/
void setName(String name);
/**
* Gets the user's hierarchy level in the system.
*
* @return the user's hierarchy level
*/
Hierarchy getHierarchy();
/**
* Sets the user's hierarchy level.
*
* @param hierarchy the hierarchy level to set
* @throws IllegalArgumentException if hierarchy is null
*/
void setHierarchy(Hierarchy hierarchy);
/**
* Provides a default natural ordering for users based on username.
*
* @param other the user to compare with
* @return negative if this username comes before, zero if equal, positive if after
*/
@Override
default int compareTo(User other) {
return this.getUsername().compareToIgnoreCase(other.getUsername());
}
/**
* Checks if the user has at least the specified hierarchy level.
*
* @param minimumHierarchy the minimum required hierarchy level
* @return true if user's hierarchy is at least the specified level
* @throws IllegalArgumentException if minimumHierarchy is null
*/
default boolean hasMinimumHierarchy(Hierarchy minimumHierarchy) {
if (minimumHierarchy == null) {
throw new IllegalArgumentException("Minimum hierarchy cannot be null");
}
return this.getHierarchy().ordinal() >= minimumHierarchy.ordinal();
}
}

View File

@@ -0,0 +1,90 @@
package server.utils;
import java.net.Socket;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.intfaces.User;
import shared.enums.ConnType;
/**
* This class represents the protocol used for processing input in the server.
*/
public class InputCommandRouter {
private static final Logger logger = Logger.getLogger(InputCommandRouter.class.getName());
/**
* Processes the input based on the given connection type, socket packet, and input string.
*
* @param connType The type of connection (DIRECT or INDIRECT).
* @param socketPacket The socket packet object.
* @param input The input string to be processed.
* @return The response as a JSON string.
*/
public static String processInput(ConnType connType, Object socketPacket, String input) {
JSONObject response = new JSONObject();
try {
JSONObject json = new JSONObject(input);
if (!json.has("command")) {
response.put("response", "Invalid command!");
return response.toString();
}
// Register the user's socket if it is not already registered
Socket socket;
if (connType == ConnType.UNICAST) {
if (json.has("username") || json.has("from")) {
User User = null;
if (json.has("username")) {
User = SystemStateManager.getUser(json.getString("username"));
}
if (json.has("from")) {
User = SystemStateManager.getUser(json.getString("from"));
}
if (User != null) {
if (socketPacket instanceof Socket) {
socket = (Socket) socketPacket;
if (socket != SystemStateManager.getUserSocket(User)) {
SystemStateManager.addUserSocket(User, socket);
}
}
}
}
}
// Process the input based on the command
switch (json.getString("command")) {
case "register":
if (connType != ConnType.UNICAST) {
return null;
}
socket = (Socket) socketPacket;
return UserHandler.register(json, socketPacket);
case "login":
if (connType != ConnType.UNICAST) {
return null;
}
socket = (Socket) socketPacket;
return UserHandler.login(json, socketPacket);
case "message":
MessageProtocolHandler.receiveMessage(connType, json);
return null;
case "request":
MessageProtocolHandler.receiveRequest(connType, json, socketPacket);
return null;
case "joinGroup":
UserHandler.joinGroup(json);
return null;
default:
logger.severe("Invalid command received! " + input);
return null;
}
} catch (JSONException e) {
logger.severe("Invalid JSON received! " + e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,260 @@
package server.utils;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.implementations.StandardMessage;
import server.structs.implementations.StandardRequest;
import server.structs.intfaces.Notification;
import server.structs.intfaces.Message;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
import server.handlers.AcceptRequestHandler;
import shared.enums.ConnType;
public class MessageProtocolHandler {
private static final Logger logger = Logger.getLogger(MessageProtocolHandler.class.getName());
private static final ExecutorService executorService = Executors.newFixedThreadPool(50);
/**
* Returns the ExecutorService used by the EventsHandler.
*
* @return the ExecutorService used by the EventsHandler
*/
public static ExecutorService getExecutorService() {
return executorService;
}
/**
* Converts an event to a JSON object.
*
* @param notification The event to convert.
* @return The JSON object representation of the event.
*/
public static <N extends Notification> JSONObject notificationToJson(N notification) throws JSONException {
JSONObject json = new JSONObject();
json.put("from", notification.getSender());
Object receiver = notification.getReceiver();
if (receiver instanceof User) {
json.put("to", ((User) receiver).getUsername());
} else if (receiver instanceof String) {
String receiverString = (String) receiver;
if (receiverString.equals("broadcast")) {
json.put("to", "broadcast");
} else if (receiverString.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
json.put("to", receiverString);
}
} else {
logger.severe("Invalid receiver type!");
}
json.put("content", notification.getMessage());
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm");
json.put("date", sdf.format(notification.getTimestamp()));
if (notification instanceof Message) {
json.put("command", "message");
return json;
} else if (notification instanceof Request) {
json.put("command", "request");
Request request = (Request) notification;
if (request.getTruster() != null) {
json.put("accepter", request.getTruster().getUsername());
} else {
json.put("accepter", "");
}
return json;
}
throw new JSONException("Invalid event type");
}
/**
* Converts a collection of events to a JSON array.
*
* @param notifications The collection of events to convert.
* @return The JSON array representation of the events.
* @throws JSONException If an error occurs while converting the events to JSON.
*/
public static JSONArray notificationsToJson(Collection<? extends Notification> notifications) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (Notification notification : notifications) {
jsonArray.put(notificationToJson(notification));
}
return jsonArray;
}
/**
* Converts a JSON object to a message.
*
* @param json The JSON object to convert.
* @return The message representation of the JSON object.
*/
public static Message messageFromJson(JSONObject json) {
try {
if (!json.has("from") || !json.has("to") || !json.has("content")) {
logger.severe("Invalid message received! (field missing)");
return null;
}
if (json.getString("from").equals("server")) {
return null;
}
User from = SystemStateManager.getUser(json.getString("from"));
if (from == null) {
logger.severe("Invalid message received! (User from)");
return null;
}
String to = json.getString("to");
String content = json.getString("content");
Message message = new StandardMessage(from, null, content);
if (to.equals("broadcast")) {
message.setReceiver(to);
} else if (to.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
message.setReceiver(to);
} else {
message.setReceiver(SystemStateManager.getUser(to));
}
if (message.getReceiver() == null) {
logger.severe("Invalid message received! (User to)");
return null;
}
return message;
} catch (JSONException ignored) {
logger.severe("Invalid message received! (JSONException)");
return null;
}
}
/**
* Converts a JSON object to a request.
*
* @param json The JSON object to convert.
* @return The request representation of the JSON object.
*/
public static Request requestFromJson(JSONObject json) {
try {
if (!json.has("from") || !json.has("to") || !json.has("content")) {
logger.severe("Invalid request received! (field missing)");
return null;
}
User from = SystemStateManager.getUser(json.getString("from"));
if (from == null) {
logger.severe("Invalid request received! (User from)");
return null;
}
String to = json.getString("to");
String content = json.getString("content");
Request request = new StandardRequest(from, null, content);
if (to.equals("broadcast")) {
request.setReceiver(to);
} else if (to.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
request.setReceiver(to);
} else {
request.setReceiver(SystemStateManager.getUser(to));
}
if (request.getReceiver() == null) {
logger.severe("Invalid request received! (User to)");
return null;
}
return request;
} catch (JSONException ignored) {
logger.severe("Invalid request received! (JSONException)");
return null;
}
}
/// ! Protocol methods
/**
* Receives a message and processes it based on the connection type and the message content.
*
* @param connType the type of connection (DIRECT or BROADCAST)
* @param json the JSON object containing the message data
* @return always returns null
*/
public static String receiveMessage(ConnType connType, JSONObject json) {
Message message = messageFromJson(json);
if (message != null) {
Object Receiver = message.getReceiver();
if (Receiver instanceof User) {
SystemStateManager.addUserNotification((User) message.getReceiver(), message);
} else if (Receiver instanceof String) {
String receiverString = (String) Receiver;
if (receiverString.equals("broadcast")) {
Collection<User> users = SystemStateManager.getUsers();
for (User user : users) {
SystemStateManager.addUserNotification(user, message);
}
} else if (receiverString.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
Collection<User> users = SystemStateManager.getUsersFromGroup(receiverString);
for (User user : users) {
SystemStateManager.addUserNotification(user, message);
}
} else {
User user = SystemStateManager.getUser(receiverString);
if (user != null) {
SystemStateManager.addUserNotification(user, message);
}
}
}
SystemStateManager.addUserNotification(message.getSender(), message);
if (connType == ConnType.UNICAST) {
SystemStateManager.addNotificationToDeliver(message);
}
}
return null;
}
/**
* Receives a request and processes it based on the connection type, JSON data, and socket packet.
*
* @param connType The type of connection (DIRECT or INDIRECT).
* @param json The JSON object containing the request data.
* @param socketPacket The socket packet associated with the request.
* @return The response string.
*/
public static String receiveRequest(ConnType connType, JSONObject json, Object socketPacket) {
Request request = requestFromJson(json);
if (request != null) {
Object Receiver = request.getReceiver();
if (Receiver instanceof User) {
SystemStateManager.addUserNotification(request.getReceiver(), request);
} else if (Receiver instanceof String) {
String receiverString = (String) Receiver;
if (receiverString.equals("broadcast")) {
Collection<User> users = SystemStateManager.getUsers();
for (User user : users) {
SystemStateManager.addUserNotification(user, request);
}
} else if (receiverString.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
Collection<User> users = SystemStateManager.getUsersFromGroup(receiverString);
for (User user : users) {
SystemStateManager.addUserNotification(user, request);
}
} else {
User user = SystemStateManager.getUser(receiverString);
if (user != null) {
SystemStateManager.addUserNotification(user, request);
}
}
}
SystemStateManager.addUserNotification(request.getSender(), request);
if (connType == ConnType.UNICAST) {
SystemStateManager.addNotificationToDeliver(request);
}
executorService.execute(new AcceptRequestHandler(connType, request));
}
return null;
}
}

View File

@@ -0,0 +1,193 @@
package server.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.Server;
import server.handlers.MessageHistoryHandler;
import server.structs.SystemStateManager;
import server.structs.intfaces.User;
import server.structs.implementations.StandardUser; // New concrete implementation
import shared.enums.Hierarchy;
/**
* Handles user registration, authentication, and communication in the emergency system.
* Manages user sessions and group memberships.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class UserHandler {
private static final Logger logger = Logger.getLogger(UserHandler.class.getName());
private static final String MULTICAST_GROUP_PATTERN =
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
// Prevent instantiation
private UserHandler() {}
/**
* Registers a new user in the system.
*
* @param json The registration details
* @param socketPacket The connection socket
* @return JSON response indicating success or failure
* @throws JSONException If registration data is malformed
*/
public static String register(JSONObject json, Object socketPacket) throws JSONException {
try {
String username = json.getString("username");
if (SystemStateManager.getUser(username) != null) {
logger.info("Registration failed: User already exists - " + username);
return createErrorResponse("User already exists!");
}
User user = createUser(json);
Socket socket = (Socket) socketPacket;
SystemStateManager.addUser(user);
SystemStateManager.addUserSocket(user, socket);
return createSuccessResponse();
} catch (IllegalArgumentException e) {
logger.log(Level.WARNING, "Registration failed: Invalid role", e);
return createErrorResponse("Invalid role!");
} catch (Exception e) {
logger.log(Level.SEVERE, "Registration failed: Unexpected error", e);
return e.getMessage();
}
}
/**
* Creates a new user instance from JSON data.
*/
private static User createUser(JSONObject json) throws JSONException {
return new StandardUser(
json.getString("username"),
json.getString("name"),
json.getString("password"),
Hierarchy.valueOf(json.getString("role").toUpperCase())
);
}
/**
* Authenticates a user and establishes their session.
*/
public static String login(JSONObject json, Object socketPacket) throws JSONException {
User user = SystemStateManager.getUser(json.getString("username"));
if (user == null) {
logger.info("Login failed: Invalid username");
return createErrorResponse("Invalid username!");
}
if (!user.getPassword().equals(json.getString("password"))) {
logger.info("Login failed: Invalid password for user " + user.getUsername());
return createErrorResponse("Invalid password!");
}
establishUserSession(user, (Socket) socketPacket);
return createSuccessResponse();
}
/**
* Sets up user session and starts message history handler.
*/
private static void establishUserSession(User user, Socket socket) {
SystemStateManager.addUserSocket(user, socket);
new Thread(new MessageHistoryHandler(user)).start();
}
/**
* Sends data to a user through their socket connection.
*/
public static void sendSomething(User user, String data) throws IOException {
try (Socket newSocket = createUserSocket(user);
PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true)) {
out.println(data);
}
}
/**
* Sends data and waits for a response.
*/
public static String sendAndReceiveSomething(User user, String data) throws IOException {
try (Socket newSocket = createUserSocket(user);
PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(newSocket.getInputStream()))) {
out.println(data);
return in.readLine();
}
}
private static Socket createUserSocket(User user) throws IOException {
Socket userSocket = SystemStateManager.getUserSocket(user);
return new Socket(userSocket.getInetAddress(), Server.USER_PORT);
}
/**
* Adds a user to a multicast group.
*/
public static String joinGroup(JSONObject json) throws JSONException {
if (!isValidGroupRequest(json)) {
return null;
}
User user = SystemStateManager.getUser(json.getString("username"));
if (user == null) {
logger.info("Group join failed: Invalid username");
return null;
}
try {
String group = json.getString("group");
SystemStateManager.getMulticastSocket().joinGroup(InetAddress.getByName(group));
SystemStateManager.addUserToGroup(group, user);
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to join multicast group", e);
}
return null;
}
private static boolean isValidGroupRequest(JSONObject json) {
if (!json.has("group") || !json.has("username")) {
logger.info("Group join failed: Missing required fields");
return false;
}
String group = json.getString("group");
if (!group.matches(MULTICAST_GROUP_PATTERN)) {
logger.info("Group join failed: Invalid group address - " + group);
return false;
}
return true;
}
private static String createSuccessResponse() {
return new JSONObject().put("response", "OK").toString();
}
private static String createErrorResponse(String message) {
return new JSONObject().put("response", message).toString();
}
public static String findUser(String username) {
User user = SystemStateManager.getUser(username);
if (user == null) {
return null;
}
return user.getUsername();
}
}

View File

@@ -0,0 +1,72 @@
package shared.enums;
import java.util.Arrays;
import java.util.Optional;
/**
* Represents different types of network connections used in the application.
*
* @author 0x1eo
* @since 2024-12-12
*/
public enum ConnType {
UNICAST("Unicast", "Point-to-point connection between two nodes"),
MULTICAST("Multicast", "One-to-many connection to a specific group of nodes"),
BROADCAST("Broadcast", "One-to-all connection reaching all nodes in the network");
private final String displayName;
private final String description;
ConnType(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
/**
* Gets the user-friendly display name of the connection type.
*
* @return the display name
*/
public String getDisplayName() {
return displayName;
}
/**
* Gets the description of the connection type.
*
* @return the description
*/
public String getDescription() {
return description;
}
/**
* Safely converts a string to a ConnType.
*
* @param value the string value to convert
* @return an Optional containing the ConnType if valid, empty Optional otherwise
*/
public static Optional<ConnType> fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return Optional.empty();
}
return Arrays.stream(values())
.filter(connType -> connType.name().equalsIgnoreCase(value.trim()))
.findFirst();
}
/**
* Checks if the connection type is suitable for group communication.
*
* @return true if the connection type supports group communication
*/
public boolean isGroupCapable() {
return this == MULTICAST || this == BROADCAST;
}
@Override
public String toString() {
return String.format("%s (%s)", displayName, description);
}
}

View File

@@ -0,0 +1,108 @@
package shared.enums;
import java.util.Arrays;
import java.util.Optional;
/**
* Represents the priority levels in the system's hierarchy.
* Used for tasks and user permissions.
*
* @author 0x1eo
* @since 2024-12-12
*/
public enum Hierarchy {
LOW(0, "Low Priority"),
MEDIUM(1, "Medium Priority"),
HIGH(2, "High Priority");
private final int value;
private final String displayName;
Hierarchy(int value, String displayName) {
this.value = value;
this.displayName = displayName;
}
/**
* Gets the numeric value associated with this hierarchy level.
*
* @return the numeric value of the hierarchy level
*/
public int getValue() {
return value;
}
/**
* Gets the display name of this hierarchy level.
*
* @return the user-friendly name of the hierarchy level
*/
public String getDisplayName() {
return displayName;
}
/**
* Finds a Hierarchy enum by its numeric value.
*
* @param value the numeric value to look up
* @return an Optional containing the Hierarchy if found, empty Optional otherwise
*/
public static Optional<Hierarchy> fromValue(int value) {
return Arrays.stream(values())
.filter(h -> h.value == value)
.findFirst();
}
/**
* Finds a Hierarchy enum by its name (case-insensitive).
*
* @param name the name to look up
* @return an Optional containing the Hierarchy if found, empty Optional otherwise
*/
public static Optional<Hierarchy> fromString(String name) {
if (name == null || name.trim().isEmpty()) {
return Optional.empty();
}
return Arrays.stream(values())
.filter(h -> h.name().equalsIgnoreCase(name.trim()))
.findFirst();
}
/**
* Gets all hierarchy values as strings.
*
* @return array of hierarchy names
*/
public static String[] getAllNames() {
return Arrays.stream(values())
.map(Hierarchy::name)
.toArray(String[]::new);
}
/**
* Gets all hierarchy display names.
*
* @return array of user-friendly hierarchy names
*/
public static String[] getAllDisplayNames() {
return Arrays.stream(values())
.map(Hierarchy::getDisplayName)
.toArray(String[]::new);
}
/**
* Compares this hierarchy level with another.
*
* @param other the hierarchy level to compare with
* @return true if this level is higher than the other
*/
public boolean isHigherThan(Hierarchy other) {
return this.value > other.value;
}
@Override
public String toString() {
return displayName;
}
}

View File

@@ -0,0 +1,90 @@
package shared.enums;
import java.util.Arrays;
import java.util.Optional;
/**
* Represents different types of message receivers in the communication system.
*
* @author 0x1eo
* @since 2024-12-12
*/
public enum RecvType {
USER("Single User", "Direct message to a specific user"),
GROUP("Group", "Message to a defined group of users"),
BROADCAST("Broadcast", "Message to all users in the network");
private final String displayName;
private final String description;
RecvType(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
/**
* Gets the user-friendly display name of the receiver type.
*
* @return the display name
*/
public String getDisplayName() {
return displayName;
}
/**
* Gets the description of the receiver type.
*
* @return the description
*/
public String getDescription() {
return description;
}
/**
* Determines if this receiver type supports multiple recipients.
*
* @return true if the receiver type supports multiple recipients
*/
public boolean isMultiReceiver() {
return this == GROUP || this == BROADCAST;
}
/**
* Safely converts a string to a RecvType.
*
* @param value the string value to convert
* @return an Optional containing the RecvType if valid, empty Optional otherwise
*/
public static Optional<RecvType> fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return Optional.empty();
}
return Arrays.stream(values())
.filter(type -> type.name().equalsIgnoreCase(value.trim()))
.findFirst();
}
/**
* Gets the appropriate receiver type for a given number of recipients.
*
* @param recipientCount the number of recipients
* @return the appropriate receiver type
*/
public static RecvType forRecipientCount(int recipientCount) {
if (recipientCount <= 0) {
throw new IllegalArgumentException("Recipient count must be positive");
}
return switch (recipientCount) {
case 1 -> USER;
case Integer.MAX_VALUE -> BROADCAST;
default -> GROUP;
};
}
@Override
public String toString() {
return String.format("%s (%s)", displayName, description);
}
}

View File

@@ -1,113 +0,0 @@
# =========================================================
# Traffic Simulation Configuration
# ---------------------------------------------------------
# All parameters controlling network layout, timing,
# and simulation behavior.
# =========================================================
# === NETWORK CONFIGURATION ===
# Intersections (each with its host and port)
intersection.Cr1.host=localhost
intersection.Cr1.port=8001
intersection.Cr2.host=localhost
intersection.Cr2.port=8002
intersection.Cr3.host=localhost
intersection.Cr3.port=8003
intersection.Cr4.host=localhost
intersection.Cr4.port=8004
intersection.Cr5.host=localhost
intersection.Cr5.port=8005
# Exit node
exit.host=localhost
exit.port=9001
# Dashboard server
dashboard.host=localhost
dashboard.port=9000
# === SIMULATION CONFIGURATION ===
# Total duration in seconds (3600 = 1 hour)
simulation.duration=60.0
# Vehicle arrival model: FIXED or POISSON
simulation.arrival.model=POISSON
# λ (lambda): average arrival rate (vehicles per second)
simulation.arrival.rate=0.5
# Fixed interval between arrivals (only used if model=FIXED)
simulation.arrival.fixed.interval=2.0
# === TRAFFIC LIGHT TIMINGS ===
# Format: trafficlight.<intersection>.<direction>.<state>=<seconds>
# Intersection 1
trafficlight.Cr1.North.green=30.0
trafficlight.Cr1.North.red=30.0
trafficlight.Cr1.South.green=30.0
trafficlight.Cr1.South.red=30.0
trafficlight.Cr1.East.green=30.0
trafficlight.Cr1.East.red=30.0
trafficlight.Cr1.West.green=30.0
trafficlight.Cr1.West.red=30.0
# Intersection 2
trafficlight.Cr2.North.green=25.0
trafficlight.Cr2.North.red=35.0
trafficlight.Cr2.South.green=25.0
trafficlight.Cr2.South.red=35.0
trafficlight.Cr2.East.green=35.0
trafficlight.Cr2.East.red=25.0
trafficlight.Cr2.West.green=35.0
trafficlight.Cr2.West.red=25.0
# Intersection 3
trafficlight.Cr3.North.green=30.0
trafficlight.Cr3.North.red=30.0
trafficlight.Cr3.South.green=30.0
trafficlight.Cr3.South.red=30.0
trafficlight.Cr3.East.green=30.0
trafficlight.Cr3.East.red=30.0
trafficlight.Cr3.West.green=30.0
trafficlight.Cr3.West.red=30.0
# Intersection 4
trafficlight.Cr4.North.green=30.0
trafficlight.Cr4.North.red=30.0
trafficlight.Cr4.South.green=30.0
trafficlight.Cr4.South.red=30.0
trafficlight.Cr4.East.green=30.0
trafficlight.Cr4.East.red=30.0
trafficlight.Cr4.West.green=30.0
trafficlight.Cr4.West.red=30.0
# Intersection 5
trafficlight.Cr5.North.green=30.0
trafficlight.Cr5.North.red=30.0
trafficlight.Cr5.South.green=30.0
trafficlight.Cr5.South.red=30.0
trafficlight.Cr5.East.green=30.0
trafficlight.Cr5.East.red=30.0
trafficlight.Cr5.West.green=30.0
trafficlight.Cr5.West.red=30.0
# === VEHICLE CONFIGURATION ===
# Probability distribution for vehicle types (must sum to 1.0)
vehicle.probability.bike=0.2
vehicle.probability.light=0.6
vehicle.probability.heavy=0.2
# Average crossing times (in seconds)
vehicle.crossing.time.bike=1.5
vehicle.crossing.time.light=2.0
vehicle.crossing.time.heavy=4.0
# === STATISTICS ===
# Interval between dashboard updates (seconds)
statistics.update.interval=10.0

View File

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

View File

@@ -1,125 +0,0 @@
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import sd.config.SimulationConfig;
import sd.engine.SimulationEngine;
import sd.model.Event;
import sd.model.EventType;
import sd.model.Intersection;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.util.StatisticsCollector;
import sd.util.VehicleGenerator;
/**
* Basic tests for the simulation components.
*/
class SimulationTest {
@Test
void testConfigurationLoading() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
assertEquals(60.0, config.getSimulationDuration());
assertEquals("POISSON", config.getArrivalModel());
assertEquals(0.5, config.getArrivalRate());
assertEquals(10.0, config.getStatisticsUpdateInterval());
}
@Test
void testVehicleGeneration() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
VehicleGenerator generator = new VehicleGenerator(config);
Vehicle vehicle = generator.generateVehicle("TEST1", 0.0);
assertNotNull(vehicle);
assertEquals("TEST1", vehicle.getId());
assertNotNull(vehicle.getType());
assertNotNull(vehicle.getRoute());
assertTrue(!vehicle.getRoute().isEmpty());
}
@Test
void testEventOrdering() {
Event e1 = new Event(5.0, EventType.VEHICLE_ARRIVAL, null, "Cr1");
Event e2 = new Event(3.0, EventType.VEHICLE_ARRIVAL, null, "Cr2");
Event e3 = new Event(7.0, EventType.TRAFFIC_LIGHT_CHANGE, null, "Cr1");
assertTrue(e2.compareTo(e1) < 0); // e2 should come before e1
assertTrue(e1.compareTo(e3) < 0); // e1 should come before e3
}
@Test
void testIntersectionVehicleQueue() {
Intersection intersection = new Intersection("TestCr");
TrafficLight light = new TrafficLight("TestCr-N", "North", 30.0, 30.0);
intersection.addTrafficLight(light);
Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
java.util.Arrays.asList("TestCr", "S"));
intersection.configureRoute("S", "North");
// Advance route to next destination
v1.advanceRoute();
intersection.receiveVehicle(v1);
assertEquals(1, intersection.getTotalQueueSize());
assertEquals(1, intersection.getTotalVehiclesReceived());
}
@Test
void testTrafficLightStateChange() {
TrafficLight light = new TrafficLight("Test-Light", "North", 30.0, 30.0);
assertEquals(TrafficLightState.RED, light.getState());
light.changeState(TrafficLightState.GREEN);
assertEquals(TrafficLightState.GREEN, light.getState());
light.changeState(TrafficLightState.RED);
assertEquals(TrafficLightState.RED, light.getState());
}
@Test
void testSimulationEngineInitialization() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
SimulationEngine engine = new SimulationEngine(config);
engine.initialize();
assertNotNull(engine.getIntersections());
assertEquals(5, engine.getIntersections().size());
// Check that intersections have traffic lights
for (Intersection intersection : engine.getIntersections().values()) {
assertEquals(3, intersection.getTrafficLights().size()); // North, South, East, West
}
}
@Test
void testStatisticsCollector() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
StatisticsCollector collector = new StatisticsCollector(config);
Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
java.util.Arrays.asList("Cr1", "Cr2", "S"));
collector.recordVehicleGeneration(v1, 0.0);
assertEquals(1, collector.getTotalVehiclesGenerated());
collector.recordVehicleArrival(v1, "Cr1", 1.0);
collector.recordVehicleExit(v1, 10.0);
assertEquals(1, collector.getTotalVehiclesCompleted());
}
}

View File

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

View File

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

View File

@@ -1,140 +0,0 @@
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());
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.