Skip to content

erichiroshi/library-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

179 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📚 Library API — Spring Boot 4 + JWT + Docker + Observability

CI PDF Quality Gate Status codecov

Java Spring Boot 4 PostgreSQL 16 Redis Docker Observability Resilience AWS S3

Backend production-ready projetado com foco em previsibilidade, observabilidade e isolamento de responsabilidades.

🔐 Autenticação JWT com Refresh Token Rotation
🧠 Arquitetura em camadas bem definida
🗄 PostgreSQL + Flyway (versionamento automático)
⚡ Cache distribuído com Redis
📊 Observabilidade completa (Micrometer + Prometheus + Grafana + Zipkin)
🧪 Testes de integração com Testcontainers (banco real)
🚀 CI/CD com quality gate obrigatório (80%+ cobertura)
☁️ Upload de imagens de capa com AWS S3


📑 Índice


📋 Requisitos

Obrigatórios

  • Docker 20.10+ & Docker Compose 2.0+
  • Git 2.30+

Opcional (apenas para rodar fora do Docker)

  • Java 25 (Eclipse Temurin recomendado)
  • Gradle 9+ (ou use o wrapper ./gradlew)

Verificar Instalação

docker --version          # Docker version 20.10+
docker compose version    # Docker Compose version 2.0+
git --version             # git version 2.30+

📑 Visão Geral

A Library API simula um backend de produção real para gerenciar livros, autores, categorias, usuários e empréstimos. Vai além de um CRUD — implementa segurança, cache distribuído, observabilidade, upload de arquivos e CI/CD completo.


🚀 Quick Start

O projeto possui dois modos de execução:

  • dev → ambiente voltado para desenvolvimento e avaliação
  • prod → ambiente containerizado simulando produção

Clone o projeto

git clone https://github.com/erichiroshi/library-api.git
cd library-api

🟢 Modo Desenvolvimento (recomendado para avaliação)

Nesse modo a infraestrutura é executada via Docker e a aplicação pode ser iniciada via container ou IDE.

1️⃣ Subir infraestrutura

docker compose -f docker-compose.dev.yml up -d

A rede library-api_backend é criada automaticamente.

Serviços iniciados:

2️⃣ Subir aplicação

Opção A — Container:

docker build -t library-api:latest .
docker run -d --network library-api_backend -p 8080:8080 --env-file .env.dev library-api:latest

Opção B — IDE:

./gradlew clean build

Refresh Gradle project → Executar a aplicação

Acesse:

Usuário admin para teste:
Email: joao.silva@email.com
Senha: 123456

Características do profile dev:

  • ✅ Swagger habilitado
  • ✅ Banco populado com seed inicial (Flyway)
  • ✅ Delay artificial de 2s no GET /books/{id} para demonstrar cache Redis
  • ✅ Access token de 30 minutos (mais conveniente)
  • ✅ Logs detalhados (DEBUG)

🏭 Modo Produção (simulado)

Executa toda a stack containerizada utilizando o profile prod.

docker compose up -d

Características do profile prod:

  • ✅ Swagger desabilitado
  • ✅ Banco de dados inicial vazio
  • ✅ Configuração mais restritiva (HikariCP tunado)
  • ✅ Ambiente totalmente containerizado
  • ✅ Stateless (JWT) + cache compartilhado (Redis)
  • ✅ Access token de 15 minutos
  • ✅ Apenas endpoints /actuator/health e /actuator/prometheus públicos

Populando banco em prod:

docker exec -i library-api-postgres-1 psql -U postgres -d library < seed_realistic_dataset.sql

🧯 Encerrar ambiente

docker compose down          # Para os containers
docker compose down -v       # Para e remove volumes (apaga banco)

🔧 Variáveis de Ambiente

Copie o arquivo de exemplo e preencha:

cp .env.example .env
Variável Descrição Exemplo
SPRING_PROFILES_ACTIVE Profile ativo prod ou dev
DB_URL URL JDBC do PostgreSQL jdbc:postgresql://postgres:5432/library
DB_USERNAME Usuário do banco postgres
DB_PASSWORD Senha do banco postgres
JWT_SECRET_KEY Chave secreta JWT (mín. 256 bits)
REDIS_HOST Host do Redis redis
REDIS_PORT Porta do Redis 6379
AWS_KEY AWS Access Key ID
AWS_SECRET AWS Secret Access Key
BUCKET_NAME Nome do bucket S3 library-api-s3
BUCKET_REGION Região do bucket sa-east-1

⚠️ O arquivo .env está no repositório apenas para fins educacionais. Em produção real, use um secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.).


📦 Postman Collection

Importe a collection para testar a API:

📁 Library-API.postman_collection.json (na raiz do projeto)


🎯 Problema que este Projeto Resolve

Este projeto vai além de um CRUD básico — ele simula desafios reais de produção:

Cenário de Negócio

Uma biblioteca precisa:

  • ✅ Gerenciar empréstimos com regras (controle de cópias disponíveis)
  • ✅ Autenticar usuários de forma segura (JWT + Refresh Token Rotation)
  • ✅ Garantir performance em consultas frequentes (Cache Redis)
  • ✅ Armazenar imagens de capa dos livros (AWS S3)
  • ✅ Monitorar saúde e métricas da aplicação (Observabilidade)
  • ✅ Garantir qualidade de código (80%+ cobertura obrigatória)
  • ✅ Evoluir schema sem quebrar produção (Flyway migrations)
  • ✅ Limpar dados expirados automaticamente (Scheduled Jobs)

Diferenciais Técnicos

  • 🔐 Segurança: JWT com token rotation (previne replay attacks)
  • ⚡ Performance: Cache distribuído com Redis + atomic decrement de cópias
  • 📊 Observabilidade: Prometheus + Grafana + OpenTelemetry + Zipkin (tracing distribuído com traceId nos logs)
  • ☁️ Storage: Upload de imagens com compressão automática via AWS S3
  • 🧪 Qualidade: 80%+ cobertura com threshold obrigatório no CI
  • 🚀 CI/CD: Quality gate automático (SonarCloud + Codecov)
  • 🐳 DevOps: Docker Compose com 6 serviços orquestrados

🛠 Stack Tecnológica

Core

  • Java 25
  • Spring Boot 4.x
    • Spring Web MVC (API REST)
    • Spring Data JPA (persistência)
    • Spring Security (JWT)
    • Spring Cache (Redis)
    • Spring Actuator (health + métricas)
  • Hibernate (ORM)
  • Lombok (redução de boilerplate)

Persistência

  • PostgreSQL 16 (banco relacional)
  • Flyway (versionamento de schema — 9 migrations)
  • Schema per Service (schemas auth, catalog e lending isolados no mesmo banco)

Cache

  • Redis 7 (cache distribuído com TTL de 2 minutos)

Storage

  • AWS S3 (upload de imagens de capa)
    • Compressão e redimensionamento automático (máx. 400px de largura)
    • Validação de content-type (PNG, JPEG, WEBP)
    • Validação de tamanho (1KB mín. / 10MB máx.)
    • Metadados automáticos no objeto S3

Observabilidade

  • Spring Actuator (health checks)
  • Micrometer (abstração de métricas)
  • Prometheus (coleta de métricas, scrape a cada 10s)
  • Grafana (dashboards provisionados automaticamente)
  • OpenTelemetry + Zipkin (tracing distribuído com traceId nos logs)

Resiliência

  • Resilience4j (Circuit Breaker + Retry para integrações externas)
  • ShedLock (lock distribuído para scheduled jobs)

Testes

  • Testcontainers (PostgreSQL real em testes de integração)
  • JUnit 5
  • Mockito
  • JaCoCo (cobertura com threshold de 80%)

Infraestrutura

  • Docker & Docker Compose (6 serviços orquestrados)
  • GitHub Actions (CI/CD — 4 workflows)
  • Dependabot (atualização automática de dependências)

Documentação e Qualidade

  • Swagger/OpenAPI 3 (habilitado no profile dev)
  • SonarCloud (quality gate)
  • Codecov (tracking de cobertura)

Serialização e Mapeamento

  • Jackson (JSON, com non_null por padrão)
  • DTOs (isolamento de domínio)
  • MapStruct (mapeamento automático)
  • Bean Validation (validação declarativa)

📐 Arquitetura

Camadas

┌─────────────────────────────────────────────┐
│         Controllers (REST Layer)            │
│   @RestController / @RequestMapping         │
│   • BookController                          │
│   • LoanController                          │
│   • AuthController                          │
│   • AuthorController                        │
│   • CategoryController                      │
└──────────────┬──────────────────────────────┘
               │ DTOs (Request/Response)
┌──────────────▼──────────────────────────────┐
│         Services (Business Logic)           │
│   @Service / @Transactional                 │
│   • BookService                             │
│   • LoanService                             │
│   • AuthService / RefreshTokenService       │
│   • BookMediaService                        │
│   • LookupServices (anti-corruption layer)  │
└──────────────┬──────────────────────────────┘
               │ Entities
┌──────────────▼──────────────────────────────┐
│      Repositories (Data Access)             │
│        JpaRepository                        │
│   • BookRepository                          │
│   • LoanRepository                          │
│   • UserRepository                          │
│   • RefreshTokenRepository                  │
└──────────────┬──────────────────────────────┘
               │
┌──────────────▼──────────────────────────────┐
│       PostgreSQL + Redis + AWS S3           │
└─────────────────────────────────────────────┘

Estrutura de Pacotes (Feature-based)

com.example.library/
├── auth/           # Autenticação (login, refresh, logout)
├── author/         # Gerenciamento de autores
├── aws/            # Integração AWS S3 + utilitários de imagem
├── book/           # Gerenciamento de livros (com cache)
├── category/       # Gerenciamento de categorias
├── common/         # BaseEntity, exceções, configurações comuns
├── config/         # CacheConfig, JpaConfig, SchedulingConfig
├── loan/           # Empréstimos e itens de empréstimo
├── refresh_token/  # Refresh tokens + cleanup job agendado
├── security/       # JWT filter, SecurityConfig, profiles dev/prod
├── swagger/        # Configuração OpenAPI
└── user/           # Entidade User + UserDetailsService

Bounded Contexts definidos

Contexto Responsabilidade Schema
auth Autenticação, usuários, refresh tokens auth
catalog Livros, autores, categorias catalog
lending Empréstimos e itens de empréstimo lending

Fluxo de Observabilidade

Application → Actuator → Micrometer → Prometheus → Grafana
     ↓                                                ↓
OpenTelemetry                                     Dashboards
     ↓
   Zipkin (tracing distribuído)

Estratégia de Cache

Request → Controller → Service → [Cache Hit? → Return]
                          ↓
                      Cache Miss
                          ↓
                     Repository → PostgreSQL
                          ↓
                      [Cache Store no Redis]

Caches configurados:

  • books — lista paginada (evict ao criar/deletar)
  • bookById — busca por ID (evict ao deletar)
  • TTL global: 2 minutos

🏗 Decisões Arquiteturais

✔ Decrement atômico de cópias

Por quê: Evitar race condition em empréstimos concorrentes.

Implementação: @Modifying com UPDATE ... WHERE availableCopies > 0 — o banco rejeita o UPDATE se não há cópias, sem necessidade de lock explícito. clearAutomatically = true invalida o cache de 1º nível do JPA após o UPDATE.

✔ Separação Controller / Service / Repository

Por quê: Evita vazamento de regra de negócio para a camada HTTP.

Benefício: Regras podem ser reutilizadas por diferentes camadas (REST, scheduled jobs, listeners).

✔ DTOs + MapStruct

Por quê: Isolamento de domínio e controle explícito de exposição.

Benefício: Entidades JPA nunca expostas diretamente — previne lazy loading exceptions e vazamento de dados sensíveis.

✔ Cache no nível de serviço

Por quê: Independente da camada web.

Benefício: Cache funciona se chamado por REST, mensageria ou scheduled job.

LoanUnauthorizedException retorna 404

Por quê: Segurança — não revelar que um empréstimo existe quando o usuário não tem permissão para acessá-lo.

✔ Delay artificial no profile dev

Por quê: Demonstrar o efeito do cache Redis de forma perceptível.

Implementação: Interface ArtificialDelayService com duas implementações — DevArtificialDelayService (2s de sleep) e NoOpArtificialDelayService — selecionadas por @Profile.

✔ Testcontainers

Por quê: Banco real nos testes de integração.

Benefício: Testes simulam produção (PostgreSQL real), não comportamento idealizado do H2 in-memory.

✔ Threshold de cobertura obrigatório

Por quê: Pipeline falha abaixo de 80%.

Benefício: Garante qualidade mínima em cada PR, evitando degradação gradual.

✔ Feature-based packages

Por quê: Preparação para extração em microservices.

Benefício: Código relacionado fica junto; cada pacote é praticamente auto-contido.

✔ Spring Events para desacoplamento de domínios

Por quê: Preparação para separação futura em microservices sem introduzir Kafka prematuramente.

Benefício: Domínios se comunicam via eventos internos (ApplicationEventPublisher) em vez de injeção direta de repositórios entre pacotes. Troca futura por Kafka/RabbitMQ requer mudança mínima.

✔ Resilience4j em integrações externas

Por quê: Proteger o monolito contra falhas de serviços externos (S3) e preparar os pontos de integração para extração futura.

Benefício: Circuit Breaker evita cascata de falhas; Retry com backoff trata falhas transitórias. Padrão já estabelecido para quando LoanService precisar chamar BookService via HTTP.

✔ Interfaces de anticorrupção entre domínios

Por quê: BookService injetava AuthorRepository e CategoryRepository diretamente. LoanService injetava BookRepository e UserRepository. Injeção direta de repositórios entre domínios cria acoplamento estrutural que impede extração futura em microservices.

Implementação: Cada domínio expõe uma interface de lookup (AuthorLookupService, CategoryLookupService, BookLookupService, UserLookupService). Outros domínios dependem da interface, nunca do repositório.

Benefício: Trocar a implementação de uma chamada local para HTTP/Feign requer mudança apenas na implementação da interface, sem tocar nos serviços consumidores.

✔ JWT Filter sem consulta ao banco

Por quê: O filtro anterior chamava userDetailsService.loadUserByUsername() em toda requisição autenticada, mesmo com as roles já presentes no JWT — gerando 1 query desnecessária por request.

Implementação: JwtAuthenticationFilter constrói o Authentication apenas com claims do token. JwtService.extractRoles() lê as roles diretamente do JWT, sem tocar no banco.

Benefício: Elimina 1 query ao banco por request autenticado.

✔ BookMediaService — separação de responsabilidades

Por quê: BookService misturava lógica de domínio com integração de infraestrutura (S3).

Implementação: BookMediaService encapsula todo o pipeline de upload: validação, redimensionamento e envio ao S3. BookService nunca depende de S3 diretamente.

Benefício: BookService testável sem mock de S3; infraestrutura substituível sem tocar em regras de negócio.

✔ Schema per Service no mesmo banco

Por quê: Microservices exigem database per service — cada serviço deve possuir e controlar suas próprias tabelas. Separar bancos imediatamente seria prematuro; separar schemas é o passo intermediário seguro.

Implementação: Três schemas criados via Flyway (V008/V009): auth, catalog e lending. Entidades anotadas com @Table(schema = "..."), tabelas de junção com @JoinTable(schema = "...") e coleções com @CollectionTable(schema = "..."). HikariCP configurado com search_path para resolução automática.

Benefício: Fronteiras de dados explícitas sem complexidade operacional de múltiplos bancos. Migração futura para bancos separados requer apenas apontar cada serviço para seu próprio PostgreSQL.


📊 Observabilidade

Métricas expostas:

  • JVM (memória, threads, GC)
  • HTTP (requests, latência, status codes)
  • Database (pool de conexões)
  • Cache Redis (hits, misses, evictions)
  • Custom de negócio (ver abaixo)

Métricas customizadas de negócio:

  • library.books.created — Counter de livros criados

Alertas configurados no Prometheus (alerts.yml):

  • HighErrorRate — taxa de erros 5xx acima de 0.05/s por 5 minutos (warning)
  • HighMemoryUsage — uso de heap JVM acima de 90% por 5 minutos (critical)

Dashboards Grafana (provisionados automaticamente):

  • Total de livros
  • Requests por segundo (RPS)
  • Requests por endpoint
  • Erros 5xx por segundo
  • Tempo médio de resposta (ms)
  • Taxa de erro (%)

Tracing Distribuído (OpenTelemetry + Zipkin)

Cada request recebe um traceId único propagado por toda a chamada e registrado nos logs.

Request → Application → OpenTelemetry SDK → Zipkin
                              ↓
                    traceId injetado nos logs (SLF4J MDC)
                              ↓
                    Zipkin UI: visualização de spans e latência

Propagação W3C Trace Context — padrão adotado, compatível com qualquer ferramenta de APM.

Acesso:


🧪 Estratégia de Testes

Pirâmide de Testes:

       /\
      /  \  E2E (poucos)
     /____\
    /      \ Integration (médio)
   /        \
  /__________\ Unit (muitos)

Unit Tests

  • Isolamento de regra de negócio
  • Mockito para dependências
  • Foco em Services

Repository Tests

  • @DataJpaTest (context slice)
  • Banco H2 in-memory (rápido)
  • Valida queries customizadas (findOverdueLoans, countActiveByUserId, decrementCopies)

Integration Tests

  • @SpringBootTest (context completo)
  • Testcontainers com PostgreSQL real
  • Profile it — cache desabilitado (@Profile("!it") no CacheConfig)
  • Valida fluxo end-to-end

Cobertura atual: 80%+
Threshold obrigatório: 80% (pipeline falha se menor)
Exclusões de cobertura: DTOs, configs, mappers gerados

Executar testes:

./gradlew test                  # Unit + Repository tests
./gradlew integrationTest       # Integration tests
./gradlew test integrationTest  # Todos os testes
./gradlew jacocoTestReport      # Gerar relatório de cobertura

Relatório HTML: build/reports/jacoco/test/html/index.html


📦 Endpoints Principais

Autenticação

Método Endpoint Descrição Auth
POST /auth/login Login — retorna access + refresh token
POST /auth/refresh Renova access token (token rotation)
POST /auth/logout Invalida o refresh token

Livros

Método Endpoint Descrição Auth
GET /api/v1/books Lista livros paginado (com cache Redis)
GET /api/v1/books/{id} Busca por ID (com cache Redis)
POST /api/v1/books Cria livro
DELETE /api/v1/books/{id} Remove livro 🔐 ADMIN
POST /api/v1/books/{id}/cover Upload de imagem de capa (S3)

Autores

Método Endpoint Descrição Auth
GET /api/v1/authors Lista autores paginado
GET /api/v1/authors/{id} Busca por ID
POST /api/v1/authors Cria autor
DELETE /api/v1/authors/{id} Remove autor 🔐 ADMIN

Categorias

Método Endpoint Descrição Auth
GET /api/v1/categories Lista categorias paginado
GET /api/v1/categories/{id} Busca por ID
POST /api/v1/categories Cria categoria 🔐 ADMIN
DELETE /api/v1/categories/{id} Remove categoria 🔐 ADMIN

Empréstimos

Método Endpoint Descrição Auth
POST /api/v1/loans Cria empréstimo
GET /api/v1/loans/{id} Busca por ID (apenas dono ou ADMIN)
GET /api/v1/loans/me Lista meus empréstimos
GET /api/v1/loans Lista todos os empréstimos 🔐 ADMIN
GET /api/v1/loans/user/{userId} Lista empréstimos por usuário 🔐 ADMIN
GET /api/v1/loans/overdue Lista empréstimos vencidos 🔐 ADMIN
PATCH /api/v1/loans/{id}/return Registra devolução
PATCH /api/v1/loans/{id}/cancel Cancela empréstimo

Documentação interativa (profile dev): http://localhost:8080/swagger-ui/index.html


☁️ Upload de Imagens (AWS S3)

Como funciona

POST /api/v1/books/{id}/cover
Content-Type: multipart/form-data

O pipeline de upload:

  1. Validação de tamanho (1KB mín. / 10MB máx.)
  2. Validação de content-type (image/png, image/jpeg, image/webp)
  3. Redimensionamento automático para máx. 400px de largura (mantém aspect ratio)
  4. Upload para S3 com metadados (uploaded-by, original-filename, upload-timestamp)
  5. URL pública salva em tb_book.cover_image_url
  6. URL retornada no header Location

Configuração AWS

Para usar o S3, você precisa de credenciais AWS com permissão de s3:PutObject e s3:GetObject no bucket configurado.

# No .env ou variáveis de ambiente:
AWS_KEY=sua-access-key
AWS_SECRET=seu-secret
BUCKET_NAME=seu-bucket
BUCKET_REGION=sa-east-1

💡 Para desenvolvimento local sem AWS, você pode usar LocalStack como alternativa.


⏰ Agendamentos (Scheduled Jobs)

RefreshTokenCleanupJob

Limpa automaticamente refresh tokens expirados do banco de dados.

  • Frequência: Todo dia às 02:00 AM (cron = "0 0 2 * * *")
  • O que faz: DELETE FROM tb_refresh_tokens WHERE expiry_date < NOW()
  • Lock distribuído: ShedLock garante execução em apenas uma instância (lockAtLeastFor = "30m", lockAtMostFor = "1h")
  • Por quê: Tokens expirados são deletados ao serem usados (via validate()), mas tokens nunca reutilizados acumulam no banco.

LoanService.markOverdue()

Marca como OVERDUE empréstimos com status = WAITING_RETURN e dueDate < hoje.

💡 O método markOverdue() está implementado no LoanService e pode ser exposto via @Scheduled ou endpoint admin conforme necessidade.


📈 Métricas do Projeto

  • ~8.000 linhas de código
  • 125+ testes (unit + integration)
  • 80%+ cobertura (JaCoCo)
  • 30+ endpoints REST versionados (/api/v1)
  • 6 serviços Docker orquestrados
  • 9 migrations Flyway
  • 4 workflows GitHub Actions (CI, Docker, Release, README PDF)
  • 3 bounded contexts isolados por schema (auth, catalog, lending)

🔮 Próximos Passos

  • Rate limiting — Resilience4j
  • OpenTelemetry + Zipkin — Tracing distribuído com W3C Trace Context
  • Bounded contexts — schemas isolados, anticorrupção e LookupServices
  • Revisão pré-Fase 3 — JWT filter otimizado, BookMediaService extraído, workflows corrigidos
  • Fase 3 — Extração em Microservices (branch microservices)
    • Config Repo + Config Server
    • Eureka Server (service discovery)
    • Spring Cloud Gateway (JWT centralizado)
    • Auth Service
    • Catalog Service
    • Loan Service
  • Deploy em cloud — AWS ECS ou Render
  • HATEOAS — Hypermedia links
  • WebSockets — Notificações real-time de devolução
  • LocalStack — Suporte a S3 local em testes de integração

📋 Acompanhe o progresso da Fase 3 no GitHub Projects


📸 Screenshots

Swagger UI

Swagger UI

Grafana Dashboard

Grafana Dashboard

Prometheus Metrics

Prometheus

Zipkin Tracing

Zipkin


🤝 Como Contribuir

Contribuições são muito bem-vindas!

Processo de Contribuição

  1. Fork o repositório
git clone https://github.com/SEU-USER/library-api.git
  1. Crie uma branch de feature
git checkout -b feature/nova-funcionalidade
  1. Faça suas mudanças

    • Adicione testes (cobertura mínima 80%)
    • Rode ./gradlew test integrationTest
    • Verifique qualidade: ./gradlew sonar
  2. Commit seguindo Conventional Commits

git commit -m "feat: adiciona endpoint de busca avançada"
  1. Push e abra um Pull Request
git push origin feature/nova-funcionalidade

PRs são revisados em até 48h com feedback construtivo garantido.


Autor

Eric Hiroshi
Backend Engineer — Java / Spring Boot


📄 Licença

Este projeto está sob a licença MIT.


Documentação em PDF

A versão em PDF é gerada automaticamente via GitHub Actions e está disponível na aba Releases e como artefato nos workflows.


"Código limpo é aquele que expressa a intenção com simplicidade e precisão."

Desenvolvido com ☕ e 💻


🌟 Star o Projeto

Se este projeto te ajudou de alguma forma, considere dar uma ⭐ no repositório!


Dúvidas? Abra uma issue ou me chame no LinkedIn!

About

API REST completa para gerenciamento de biblioteca com Spring Boot e Java 25. Inclui autenticação JWT, cache Redis, observabilidade (Prometheus/Grafana), Docker Compose e CI/CD.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors