PULSOFT

1.Rest Crud Basico

1.spring basico

Primer contacto con Spring Boot y el desarrollo de APIs REST.

En este ejercicio se implementa un controlador básico para practicar los principales mecanismos de Spring Boot: creación de endpoints, recepción de parámetros mediante URL, uso de variables de ruta y procesamiento de objetos JSON enviados en el cuerpo de la petición.

La aplicación incluye varios ejemplos prácticos como saludos personalizados, una calculadora sencilla, validación de credenciales y recepción de datos de usuarios, permitiendo familiarizarse con las anotaciones más utilizadas en el desarrollo de servicios REST.

Conceptos trabajados:

* @RestController
* @GetMapping
* @PostMapping
* @PathVariable
* @RequestParam
* @RequestBody
* Creación de modelos Java
* Manejo básico de peticiones HTTP
* Validación de datos de entrada

package es.pulsoft.spring_basico.controller;

import es.pulsoft.spring_basico.model.Usuario;
import org.springframework.web.bind.annotation.*;

/**
 * Controller básico para practicar conceptos fundamentales de Spring Boot REST.
 * Incluye ejemplos de:
 * - @GetMapping con PathVariable
 * - @RequestParam para parámetros de consulta
 * - @RequestBody para recibir JSON
 */
@RestController
public class BasicoController {

    /**
     * Endpoint simple de saludo.
     * GET /saludo
     */
    @GetMapping("/saludo")
    public String saludo() {
        return "Hola, bienvenido a la API";
    }

    /**
     * Endpoint con PathVariable para saludo personalizado.
     * Ejemplo: /saludo/Pul
     *
     * @param nombre nombre recibido en la URL
     * @return mensaje personalizado
     */
    @GetMapping("/saludo/{nombre}")
    public String saludoPersonalizado(@PathVariable String nombre) {
        return "Hola " + nombre;
    }

    /**
     * Endpoint de calculadora básica.
     * Permite suma o resta según el parámetro "op".
     *
     * Ejemplo:
     * /calcular?op=suma&num1=5&num2=10
     *
     * @param op operación (suma o resta)
     * @param num1 primer número
     * @param num2 segundo número
     * @return resultado de la operación
     */
    @GetMapping("/calcular")
    public int calcularSuma(@RequestParam String op,
                            @RequestParam int num1,
                            @RequestParam int num2) {

        if (op.equalsIgnoreCase("suma")) {
            return num1 + num2;
        } else if (op.equalsIgnoreCase("resta")) {
            return num1 - num2;
        } else {
            throw new IllegalArgumentException("Operación no válida");
        }
    }

    /**
     * Endpoint que recibe un usuario en formato JSON.
     *
     * Ejemplo de body:
     * {
     *   "nombre": "Pul",
     *   "edad": 20
     * }
     *
     * @param usuario objeto recibido desde el body
     * @return mensaje confirmando recepción
     */
    @PostMapping("/usuario")
    public String datosUsuario(@RequestBody Usuario usuario) {
        return "Usuario recibido: " + usuario.getNombre()
                + " tiene " + usuario.getEdad() + " años";
    }

    /**
     * Endpoint de login simple.
     * Simula autenticación básica con usuario y contraseña fijos.
     *
     * Ejemplo:
     * /login?user=admin&pass=1234
     *
     * @param user usuario
     * @param pass contraseña
     * @return resultado del login
     */
    @GetMapping("/login")
    public String validarLogin(@RequestParam String user,
                               @RequestParam String pass) {

        if (user.equals("admin") && pass.equals("1234")) {
            return "Login correcto";
        } else {
            return "Login incorrecto";
        }
    }
}
package es.pulsoft.spring_basico.model;

public class Usuario {

    private String nombre;
    private int edad;

    public Usuario() {

    }

    public Usuario(String nombre,int edad) {
        this.nombre = nombre;
        this.edad=edad;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public int getEdad() {
        return edad;
    }

    public void setEdad(int edad) {
        this.edad = edad;
    }
}
2.spring peliculas

API de Películas con Validaciones

Descripción

Desarrollo de una API REST para la gestión de películas utilizando Spring Boot y almacenamiento temporal en memoria mediante una colección ArrayList.

Conceptos trabajados

- Creación de controladores REST.
- Uso de @GetMapping, @PostMapping y @DeleteMapping.
- Recepción de parámetros mediante URL y JSON.
- Validaciones manuales de datos.
- Gestión de errores con ResponseStatusException.
- Uso de ResponseEntity.
- Búsquedas y filtrados mediante Streams.

Entidad principal

La aplicación gestiona películas con la siguiente información:

- Identificador.
- Título.
- Director.
- Duración.
- Estado de estreno.

Funcionalidades implementadas

- Listado completo de películas.
- Consulta de una película por identificador.
- Búsqueda de películas por director.
- Alta de nuevas películas mediante peticiones JSON.
- Eliminación de películas existentes.

Validaciones aplicadas

- El título es obligatorio.
- El director es obligatorio.
- La duración debe ser mayor que cero.
- No se permiten películas con títulos duplicados.

Gestión de errores

La aplicación devuelve respuestas HTTP adecuadas utilizando ResponseStatusException para gestionar errores de validación y recursos inexistentes.

package es.pulsoft.spring_peliculas.controller;

import es.pulsoft.spring_peliculas.model.Pelicula;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;

/**
 * Controlador REST para gestionar películas en memoria.
 *
 * Conceptos practicados:
 * - GET, POST y DELETE
 * - PathVariable
 * - RequestParam
 * - RequestBody
 * - ResponseEntity
 * - ResponseStatusException
 * - Streams
 * - Validaciones básicas
 */
@RestController
public class PeliculaController {

    /** Contador para generar IDs automáticos. */
    private int contadorIds = 1;

    /** Lista que simula una base de datos en memoria. */
    List<Pelicula> listaPeliculas = new ArrayList<>();

    /**
     * Obtiene todas las películas almacenadas.
     *
     * GET /peliculas
     *
     * @return lista completa de películas
     */
    @GetMapping("/peliculas")
    public List<Pelicula> listaPeliculas() {
        return listaPeliculas;
    }

    /**
     * Busca una película por su ID.
     *
     * GET /peliculas/{id}
     *
     * @param id identificador de la película
     * @return película encontrada
     */
    @GetMapping("/peliculas/{id}")
    public Pelicula buscarId(@PathVariable int id) {
        return buscarPorId(id);
    }

    /**
     * Método auxiliar para localizar una película por ID.
     *
     * Si no existe, lanza una excepción 404.
     *
     * @param id identificador de la película
     * @return película encontrada
     */
    private Pelicula buscarPorId(int id) {
        return listaPeliculas.stream()
                .filter(n -> n.getId() == id)
                .findFirst()
                .orElseThrow(() ->
                        new ResponseStatusException(
                                HttpStatus.NOT_FOUND,
                                "Pelicula no encontrada"));
    }

    /**
     * Filtra películas por director.
     *
     * Ejemplo:
     * GET /peliculas/buscar?director=Nolan
     *
     * @param director nombre del director
     * @return lista de películas del director indicado
     */
    @GetMapping("/peliculas/buscar")
    public List<Pelicula> listarDirector(@RequestParam String director) {

        return listaPeliculas.stream()
                .filter(n -> n.getDirector().equalsIgnoreCase(director))
                .toList();
    }

    /**
     * Añade una nueva película a la lista.
     *
     * - Valida los datos recibidos.
     * - Asigna un ID automático.
     * - Devuelve 201 CREATED si todo es correcto.
     *
     * @param pelicula película recibida en formato JSON
     * @return película creada
     */
    @PostMapping("/peliculas")
    public ResponseEntity<Pelicula> agregarPelicula(@RequestBody Pelicula pelicula) {

        if (validaciones(pelicula)) {

            pelicula.setId(contadorIds++);
            listaPeliculas.add(pelicula);

            return ResponseEntity
                    .status(HttpStatus.CREATED)
                    .body(pelicula);
        }

        throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST,
                "Error al agregar la pelicula");
    }

    /**
     * Realiza validaciones básicas:
     * - Título obligatorio.
     * - Director obligatorio.
     * - Duración mayor que cero.
     * - No permite títulos duplicados.
     *
     * @param pelicula película a validar
     * @return true si es válida, false en caso contrario
     */
    private boolean validaciones(Pelicula pelicula) {

        if (pelicula.getTitulo() != null
                && !pelicula.getTitulo().isBlank()
                && pelicula.getDirector() != null
                && !pelicula.getDirector().isBlank()
                && pelicula.getDuracion() > 0) {

            for (Pelicula p : listaPeliculas) {

                if (p.getTitulo()
                        .equalsIgnoreCase(pelicula.getTitulo())) {
                    return false;
                }
            }

            return true;
        }

        return false;
    }

    /**
     * Elimina una película por ID.
     *
     * GET /peliculas/{id}
     *
     * @param id identificador de la película
     * @return película eliminada
     */
    @DeleteMapping("/peliculas/{id}")
    public ResponseEntity<Pelicula> borrarPelicula(@PathVariable int id) {

        Pelicula p = buscarPorId(id);

        listaPeliculas.remove(p);

        return ResponseEntity
                .status(HttpStatus.OK)
                .body(p);
    }
}
package es.pulsoft.spring_peliculas.model;

public class Pelicula {

    private int id;
    private String titulo;
    private String director;
    private int duracion;
    private boolean estrenada;

    public Pelicula() {

    }
    public Pelicula(int id, String titulo, String director, int duracion, boolean estrenada) {
        this.id = id;
        this.titulo = titulo;
        this.director = director;
        this.duracion = duracion;
        this.estrenada = estrenada;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public String getDirector() {
        return director;
    }

    public void setDirector(String director) {
        this.director = director;
    }

    public int getDuracion() {
        return duracion;
    }

    public void setDuracion(int duracion) {
        this.duracion = duracion;
    }

    public boolean isEstrenada() {
        return estrenada;
    }

    public void setEstrenada(boolean estrenada) {
        this.estrenada = estrenada;
    }
}
3.spring libros

API de Libros (CRUD Completo)

Descripción

Desarrollo de una API REST para la gestión de libros utilizando Spring Boot, aplicando operaciones CRUD completas y almacenamiento en memoria mediante una colección ArrayList.

Conceptos trabajados

- Creación de controladores REST.
- Uso de GET, POST, PUT, PATCH y DELETE.
- Manejo de ResponseEntity para respuestas HTTP.
- Gestión de errores con ResponseStatusException.
- Validaciones manuales de datos.
- Uso de Streams para búsquedas y filtrados.
- Generación de IDs automáticos.
- Reutilización de métodos internos.

Entidad principal

La aplicación gestiona libros con la siguiente estructura:

- Identificador.
- Título.
- Autor.
- Número de páginas.
- Estado de disponibilidad.

Almacenamiento

Se utiliza una lista en memoria:

- List<Libro> listaLibros = new ArrayList<>();
- Control de IDs mediante contador automático.

Funcionalidades implementadas

- Listado completo de libros.
- Consulta de libro por ID.
- Búsqueda de libros por autor.
- Creación de nuevos libros.
- Actualización completa de registros.
- Actualización parcial de disponibilidad.
- Eliminación de libros.

Endpoints

- GET /libros → obtener todos los libros.
- GET /libros/{id} → obtener libro por ID.
- GET /libros/buscar?autor= → búsqueda por autor.
- POST /libros → crear libro.
- PUT /libros/{id} → actualización completa.
- PATCH /libros/{id} → actualización parcial.
- DELETE /libros/{id} → eliminar libro.

Validaciones aplicadas

- El título es obligatorio.
- El autor es obligatorio.
- Las páginas deben ser mayores que 0.
- No se permiten títulos duplicados.

Gestión de errores

Se utilizan ResponseStatusException para controlar errores de validación y recursos no encontrados, devolviendo códigos HTTP adecuados.

Extras

Las respuestas de creación, actualización y eliminación se devuelven mediante ResponseEntity para un mejor control de los estados HTTP.

package es.pulsoft.spring_libros.controller;

import es.pulsoft.spring_libros.model.Libro;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;

/**
 * Controlador REST para la gestión de libros.
 *
 * Este controlador simula una API en memoria sin base de datos,
 * incluyendo operaciones CRUD básicas y filtros.
 *
 * Conceptos aplicados:
 * - REST API con Spring Boot
 * - Operaciones CRUD (GET, POST, PUT, PATCH, DELETE)
 * - Manejo de errores con ResponseStatusException
 * - Validaciones manuales
 * - Streams para filtrado de datos
 */
@RestController
public class LibroController {

    /** Contador simple para generación de IDs automáticos */
    private int contadorIds = 1;

    /** Lista en memoria que simula una base de datos */
    List<Libro> listaLibros = new ArrayList<>();

    /**
     * Obtiene todos los libros almacenados.
     *
     * GET /libros
     */
    @GetMapping("/libros")
    public List<Libro> listarLibros() {
        return listaLibros;
    }

    /**
     * Busca un libro por su identificador único.
     *
     * GET /libros/{id}
     *
     * @param id identificador del libro
     * @return libro encontrado
     */
    @GetMapping("/libros/{id}")
    public Libro buscarLibroId(@PathVariable int id) {
        return buscarPorId(id);
    }

    /**
     * Método auxiliar reutilizable para búsqueda por ID.
     *
     * Lanza excepción 404 si el libro no existe.
     *
     * @param id identificador del libro
     */
    private Libro buscarPorId(int id) {
        return listaLibros.stream()
                .filter(n -> n.getId() == id)
                .findFirst()
                .orElseThrow(() ->
                        new ResponseStatusException(
                                HttpStatus.NOT_FOUND,
                                "Libro no encontrado"));
    }

    /**
     * Filtra libros por autor.
     *
     * GET /libros/buscar?autor=Nombre
     *
     * @param autor nombre del autor
     * @return lista de libros del autor
     */
    @GetMapping("/libros/buscar")
    public List<Libro> buscarAutor(@RequestParam String autor) {
        return listaLibros.stream()
                .filter(n -> n.getAutor().equalsIgnoreCase(autor))
                .toList();
    }

    /**
     * Crea un nuevo libro en el sistema.
     *
     * POST /libros
     *
     * Realiza validaciones básicas y comprueba duplicados.
     *
     * @param libro objeto recibido en el body
     * @return libro creado con ID asignado
     */
    @PostMapping("/libros")
    public ResponseEntity<Libro> crearLibro(@RequestBody Libro libro) {

        if (validaciones(libro) && duplicados(libro)) {

            libro.setId(contadorIds++);
            listaLibros.add(libro);

            return ResponseEntity.status(HttpStatus.CREATED).body(libro);
        }

        throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST,
                "El libro no se creara");
    }

    /**
     * Valida los campos obligatorios del libro.
     *
     * Reglas:
     * - título obligatorio
     * - autor obligatorio
     * - páginas mayor que 0
     *
     * @param libro libro a validar
     */
    private boolean validaciones(Libro libro) {

        if (libro.getTitulo() == null || libro.getTitulo().isBlank()) {
            throw new ResponseStatusException(
                    HttpStatus.BAD_REQUEST,
                    "El titulo esta en blanco.");
        }

        if (libro.getAutor() == null || libro.getAutor().isBlank()) {
            throw new ResponseStatusException(
                    HttpStatus.BAD_REQUEST,
                    "El autor esta en blanco.");
        }

        if (libro.getPaginas() <= 0) {
            throw new ResponseStatusException(
                    HttpStatus.BAD_REQUEST,
                    "El libro no tiene paginas.");
        }

        return true;
    }

    /**
     * Verifica que no existan títulos duplicados en memoria.
     *
     * @param libro libro a comprobar
     */
    private boolean duplicados(Libro libro) {

        for (Libro l : listaLibros) {

            if (l.getTitulo().equalsIgnoreCase(libro.getTitulo())) {
                throw new ResponseStatusException(
                        HttpStatus.BAD_REQUEST,
                        "El libro esta duplicado.");
            }
        }

        return true;
    }

    /**
     * Actualiza completamente un libro existente.
     *
     * PUT /libros/{id}
     *
     * Sustituye todos los campos del libro.
     *
     * @param id identificador del libro
     * @param libro datos nuevos del libro
     */
    @PutMapping("/libros/{id}")
    public ResponseEntity<Libro> actualizarLibro(@PathVariable int id,
                                                 @RequestBody Libro libro) {

        Libro l = buscarLibroId(id);

        if (validaciones(libro)) {

            l.setTitulo(libro.getTitulo());
            l.setAutor(libro.getAutor());
            l.setPaginas(libro.getPaginas());
            l.setDisponible(libro.isDisponible());

            return ResponseEntity.status(HttpStatus.OK).body(l);
        }

        throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST,
                "El libro no se actualizara");
    }

    /**
     * Actualiza parcialmente la disponibilidad de un libro.
     *
     * PATCH /libros/{id}?disponible=true
     *
     * @param id identificador del libro
     * @param disponible nuevo estado de disponibilidad
     */
    @PatchMapping("/libros/{id}")
    public Libro actualizarDisponibilidad(@PathVariable int id,
                                          @RequestParam boolean disponible) {

        Libro l = buscarLibroId(id);
        l.setDisponible(disponible);
        return l;
    }

    /**
     * Elimina un libro por su ID.
     *
     * DELETE /libros/{id}
     *
     * @param id identificador del libro
     * @return libro eliminado
     */
    @DeleteMapping("/libros/{id}")
    public ResponseEntity<Libro> borrarLibro(@PathVariable int id) {

        Libro l = buscarLibroId(id);
        listaLibros.remove(l);

        return ResponseEntity.status(HttpStatus.OK).body(l);
    }
}
package es.pulsoft.spring_libros.model;

public class Libro {

    private int id;
    private String titulo;
    private String autor;
    private int paginas;
    private boolean disponible;

    public Libro(){

    }
    public Libro(int id, String titulo, String autor, int paginas, boolean disponible) {
        this.id = id;
        this.titulo = titulo;
        this.autor = autor;
        this.paginas = paginas;
        this.disponible = disponible;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public String getAutor() {
        return autor;
    }

    public void setAutor(String autor) {
        this.autor = autor;
    }

    public int getPaginas() {
        return paginas;
    }

    public void setPaginas(int paginas) {
        this.paginas = paginas;
    }

    public boolean isDisponible() {
        return disponible;
    }

    public void setDisponible(boolean disponible) {
        this.disponible = disponible;
    }
}
4.spring productos

API de Productos (CRUD avanzado en memoria)

Descripción

Desarrollo de una API REST en Spring Boot para la gestión de productos, implementando un CRUD avanzado en memoria con validaciones, filtros y ordenaciones mediante Streams.

El proyecto está diseñado sin capas adicionales (Service/Repository), centralizando toda la lógica en el controlador.

Conceptos trabajados

- Creación de controladores REST con @RestController.
- Uso de métodos HTTP: GET, POST, PUT, PATCH y DELETE.
- Manejo de ResponseEntity para respuestas HTTP controladas.
- Gestión de errores con ResponseStatusException.
- Uso de ArrayList como almacenamiento en memoria.
- Generación automática de identificadores.
- Uso de Streams para filtros, búsquedas y ordenaciones.
- Validaciones manuales de datos.
- Control de duplicados.

Entidad principal

La aplicación gestiona productos con la siguiente estructura:

- Identificador.
- Nombre.
- Categoría.
- Stock disponible.
- Precio.
- Estado activo/inactivo.

Almacenamiento

- List<Producto> listaProductos = new ArrayList<>();
- Control de IDs mediante contador automático.

Funcionalidades implementadas

- Listado completo de productos.
- Consulta de producto por ID.
- Creación de nuevos productos.
- Búsqueda por categoría.
- Filtrado por rango de stock.
- Filtrado por estado activo.
- Ordenación de productos por diferentes campos.
- Actualización completa de productos.
- Actualización parcial de campos específicos.
- Eliminación de productos.

Endpoints

- GET /productos → obtener todos los productos.
- GET /productos/{id} → obtener producto por ID.
- POST /productos → crear producto.
- GET /productos/buscar?categoria= → filtrar por categoría.
- GET /productos/stock?min=...&max=... → filtrar por stock.
- GET /productos/activos?estado= → filtrar por estado activo.
- GET /productos/ordenar?campo= → ordenar por campo.
- PUT /productos/{id} → actualización completa.
- PATCH /productos/{id} → actualización parcial.
- DELETE /productos/{id} → eliminar producto.

Validaciones aplicadas

- El nombre es obligatorio.
- La categoría es obligatoria.
- El stock no puede ser negativo.
- El precio debe ser mayor que 0.
- No se permiten nombres duplicados.

Gestión de errores

Se utiliza ResponseStatusException para controlar errores de validación, duplicados y recursos inexistentes, devolviendo códigos HTTP adecuados según cada caso.

package es.pulsoft.spring_productos.controller;

import es.pulsoft.spring_productos.model.Producto;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@RestController
public class ProductoController {

    // Contador simple para generar IDs únicos en memoria (simulación de base de datos)
    private int contadorIds = 1;

    // Lista en memoria que actúa como repositorio temporal de productos
    private List<Producto> listaProductos = new ArrayList<>();

    /**
     * Obtiene todos los productos almacenados
     */
    @GetMapping("/productos")
    public List<Producto> listarProductos() {
        return listaProductos;
    }

    /**
     * Busca un producto por su ID
     */
    @GetMapping("/productos/{id}")
    public Producto buscarProducto(@PathVariable int id) {
        return buscarPorId(id);
    }

    /**
     * Método auxiliar para búsqueda de producto por ID
     * Lanza excepción 404 si no existe
     */
    private Producto buscarPorId(int id) {
        return listaProductos.stream()
                .filter(n -> n.getId() == id)
                .findFirst()
                .orElseThrow(() ->
                        new ResponseStatusException(HttpStatus.NOT_FOUND,
                                "Producto no encontrado"));
    }

    /**
     * Validaciones generales para creación/actualización completa (PUT)
     * Reglas de negocio básicas:
     * - nombre obligatorio
     * - categoría obligatoria
     * - stock >= 0
     * - precio > 0
     */
    private boolean validaciones(Producto producto) {

        if (producto.getNombre() == null || producto.getNombre().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El nombre del producto no puede quedar vacio.");
        }

        if (producto.getCategoria() == null || producto.getCategoria().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "La categoria del producto no puede quedar vacia.");
        }

        if (producto.getStock() < 0) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El stock no puede ser negativo.");
        }

        if (producto.getPrecio() <= 0) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El precio tiene que ser mayor que 0");
        }

        return true;
    }

    /**
     * Validación de duplicados por nombre (case-insensitive)
     * Usado en creación de productos
     */
    private boolean duplicados(Producto producto) {
        Producto p = listaProductos.stream()
                .filter(n -> n.getNombre().equalsIgnoreCase(producto.getNombre()))
                .findFirst()
                .orElse(null);

        if (p != null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El Producto ya esta dado de alta.");
        }

        return true;
    }

    /**
     * Crear un nuevo producto
     */
    @PostMapping("/productos")
    public ResponseEntity<Producto> crearProducto(@RequestBody Producto producto) {

        if (validaciones(producto) && duplicados(producto)) {

            producto.setId(contadorIds++);
            listaProductos.add(producto);

            return ResponseEntity.status(HttpStatus.CREATED).body(producto);
        }

        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Producto no creado");
    }

    /**
     * Filtra productos por categoría
     */
    @GetMapping("/productos/categoria")
    public List<Producto> filtrarCategoria(@RequestParam String categoria) {
        return listaProductos.stream()
                .filter(n -> n.getCategoria().equalsIgnoreCase(categoria))
                .toList();
    }

    /**
     * Filtra productos por rango de stock
     */
    @GetMapping("/productos/stock")
    public List<Producto> filtrarStock(@RequestParam int minStock,
                                       @RequestParam int maxStock) {

        if (maxStock < minStock) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El stock maximo tiene que ser mayor que el minimo");
        }

        return listaProductos.stream()
                .filter(n -> n.getStock() >= minStock && n.getStock() <= maxStock)
                .toList();
    }

    /**
     * Filtra productos por estado (activo/inactivo)
     */
    @GetMapping("/productos/estado")
    public List<Producto> filtrarEstado(@RequestParam boolean estado) {
        return listaProductos.stream()
                .filter(n -> n.isActivo() == estado)
                .toList();
    }

    /**
     * Ordena productos por un campo específico
     */
    @GetMapping("/productos/ordenar")
    public List<Producto> ordenarProductos(@RequestParam String campo) {

        switch (campo.toLowerCase()) {

            case "precio" -> {
                return listaProductos.stream()
                        .sorted((a, b) -> Double.compare(a.getPrecio(), b.getPrecio()))
                        .toList();
            }

            case "stock" -> {
                return listaProductos.stream()
                        .sorted((a, b) -> Integer.compare(a.getStock(), b.getStock()))
                        .toList();
            }

            default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El campo de ordenacion no es valido.");
        }
    }

    /**
     * Actualización completa de producto (PUT)
     * Sustituye todos los campos del recurso
     */
    @PutMapping("/productos/{id}")
    public ResponseEntity<Producto> actualizarProducto(@PathVariable int id,
                                                       @RequestBody Producto producto) {

        if (validaciones(producto) && duplicadosActualizaciones(id, producto)) {

            Producto p = buscarPorId(id);

            p.setNombre(producto.getNombre());
            p.setCategoria(producto.getCategoria());
            p.setStock(producto.getStock());
            p.setPrecio(producto.getPrecio());
            p.setActivo(producto.isActivo());

            return ResponseEntity.status(HttpStatus.OK).body(p);
        }

        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Producto no actualizado");
    }

    /**
     * Validación de duplicados en actualización (excluyendo el propio ID)
     */
    private boolean duplicadosActualizaciones(int id, Producto producto) {
        for (Producto p : listaProductos) {

            if (p.getNombre().equalsIgnoreCase(producto.getNombre())
                    && p.getId() != id) {

                throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                        "El producto no se actualizara para evitar duplicados.");
            }
        }

        return true;
    }

    /**
     * Actualización parcial (PATCH)
     * Permite modificar solo campos concretos del producto:
     * - precio
     * - stock
     * - activo
     *
     * Usa Map para permitir actualización flexible sin DTO
     */
    @PatchMapping("/productos/{id}")
    public Producto actualizacionParcial(@RequestBody Map<String, Object> datos,
                                         @PathVariable int id) {

        Producto p = buscarPorId(id);

        // Actualización de precio con validación
        if (datos.containsKey("precio")) {

            Object valor = datos.get("precio");

            if (valor instanceof Number) {

                double precio = ((Number) valor).doubleValue();

                if (precio <= 0) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                            "El precio tiene que ser mayor 0");
                }

                p.setPrecio(precio);
            }
        }

        // Actualización de stock con validación
        if (datos.containsKey("stock")) {

            Object valor = datos.get("stock");

            if (valor instanceof Number) {

                int stock = ((Number) valor).intValue();

                if (stock < 0) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                            "El stock no puede ser negativo.");
                }

                p.setStock(stock);
            }
        }

        // Actualización de estado activo/inactivo
        if (datos.containsKey("activo")) {

            Object valor = datos.get("activo");

            if (valor instanceof Boolean) {
                p.setActivo((Boolean) valor);
            }
        }

        return p;
    }

    /**
     * Elimina un producto por ID
     */
    @DeleteMapping("/productos/{id}")
    public ResponseEntity<Producto> borrarProducto(@PathVariable int id) {

        Producto p = buscarPorId(id);
        listaProductos.remove(p);

        return ResponseEntity.status(HttpStatus.OK).body(p);
    }
}
package es.pulsoft.spring_productos.model;

public class Producto {

    private int id;
    private String nombre;
    private String categoria;
    private int stock;
    private double precio;
    private boolean activo;

    public Producto() {

    }

    public Producto(int id, String nombre, String categoria, int stock, double precio, boolean activo) {
        this.id = id;
        this.nombre = nombre;
        this.categoria = categoria;
        this.stock = stock;
        this.precio = precio;
        this.activo = activo;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getCategoria() {
        return categoria;
    }

    public void setCategoria(String categoria) {
        this.categoria = categoria;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }

    public double getPrecio() {
        return precio;
    }

    public void setPrecio(double precio) {
        this.precio = precio;
    }

    public boolean isActivo() {
        return activo;
    }

    public void setActivo(boolean activo) {
        this.activo = activo;
    }
}
5.spring clientes

Gestión de clientes (CRUD básico sin base de datos)

Descripción

Desarrollo de una API REST en Spring Boot para la gestión de clientes, utilizando almacenamiento en memoria mediante una lista y aplicando operaciones CRUD completas con validaciones manuales.

El proyecto se implementa en un único controlador, sin capas adicionales de servicio o repositorio.

Conceptos trabajados

- Creación de controladores REST con @RestController.
- Uso de métodos HTTP: GET, POST, PUT, PATCH y DELETE.
- Manejo de listas en memoria (ArrayList).
- Validaciones manuales de datos.
- Gestión de errores con ResponseStatusException.
- Uso de ResponseEntity.
- Filtrado de datos mediante Streams.
- Actualización parcial de recursos.

Entidad principal

La aplicación gestiona clientes con la siguiente estructura:

- Identificador.
- Nombre.
- Email.
- Edad.
- Estado activo/inactivo.

Almacenamiento

- List<Cliente> listaClientes = new ArrayList<>();
- Generación de IDs automática mediante contador.

Funcionalidades implementadas

- Listado completo de clientes.
- Consulta de cliente por ID.
- Filtrado por rango de edad.
- Filtrado por estado activo/inactivo.
- Creación de clientes.
- Actualización completa de clientes.
- Actualización parcial de campos específicos.
- Eliminación de clientes.

Endpoints

- GET /clientes → listado de clientes.
- GET /clientes/{id} → cliente por ID.
- GET /clientes/edad?min=&max= → filtrado por edad.
- GET /clientes/activos?estado= → filtrado por estado.
- POST /clientes → creación de cliente.
- PUT /clientes/{id} → actualización completa.
- PATCH /clientes/{id} → actualización parcial.
- DELETE /clientes/{id} → eliminación de cliente.

Validaciones aplicadas

- El nombre no puede estar vacío.
- El email no puede estar vacío.
- La edad debe ser mayor que 0.
- No se permiten emails duplicados.

Gestión de errores

Se utiliza ResponseStatusException para controlar errores de validación, recursos inexistentes y duplicados, devolviendo códigos HTTP adecuados (400 y 404 según el caso).

package es.pulsoft.spring_clientes.controller;

import es.pulsoft.spring_clientes.model.Cliente;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Controlador REST encargado de la gestión de clientes en memoria.
 * Incluye operaciones CRUD y filtros básicos.
 */
@RestController
public class ClienteController {

    private int contadorIds = 1;
    private final List<Cliente> listaClientes = new ArrayList<>();

    /**
     * Devuelve el listado completo de clientes.
     */
    @GetMapping("/clientes")
    public List<Cliente> listarClientes() {
        return listaClientes;
    }

    /**
     * Busca un cliente por su identificador.
     */
    @GetMapping("/clientes/{id}")
    public Cliente buscarCliente(@PathVariable int id) {
        return buscarPorId(id);
    }

    /**
     * Búsqueda interna por ID con control de error si no existe.
     */
    private Cliente buscarPorId(int id) {
        return listaClientes.stream()
                .filter(n -> n.getId() == id)
                .findFirst()
                .orElseThrow(() ->
                        new ResponseStatusException(HttpStatus.NOT_FOUND,
                                "Cliente no encontrado."));
    }

    /**
     * Filtra clientes por rango de edad.
     */
    @GetMapping("/clientes/edad/")
    public List<Cliente> filtrarEdad(@RequestParam int minEdad,
                                     @RequestParam int maxEdad) {

        if (maxEdad < minEdad) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "La edad máxima no puede ser inferior a la mínima.");
        }

        return listaClientes.stream()
                .filter(n -> n.getEdad() >= minEdad && n.getEdad() <= maxEdad)
                .toList();
    }

    /**
     * Filtra clientes por estado activo/inactivo.
     */
    @GetMapping("/clientes/activos/")
    public List<Cliente> filtrarActivos(@RequestParam boolean activo) {
        return listaClientes.stream()
                .filter(n -> n.isActivo() == activo)
                .toList();
    }

    /**
     * Validaciones básicas del cliente antes de persistir o actualizar.
     */
    private boolean validaciones(Cliente cliente) {
        if (cliente.getNombre() == null || cliente.getNombre().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El nombre del cliente no puede quedar vacío.");
        }

        if (cliente.getEmail() == null || cliente.getEmail().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "El email del cliente no puede quedar vacío.");
        }

        if (cliente.getEdad() <= 0) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "La edad del cliente no puede ser menor de 1.");
        }

        return true;
    }

    /**
     * Comprueba que no exista otro cliente con el mismo email.
     */
    private boolean duplicados(Cliente cliente) {

        listaClientes.stream()
                .filter(n -> n.getEmail().equalsIgnoreCase(cliente.getEmail()))
                .findFirst()
                .ifPresent(n -> {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                            "Cliente con email duplicado.");
                });

        return true;
    }

    /**
     * Alta de nuevo cliente.
     */
    @PutMapping("/clientes")
    public ResponseEntity<Cliente> agregarCliente(@RequestBody Cliente cliente) {

        if (validaciones(cliente) && duplicados(cliente)) {
            cliente.setId(contadorIds++);
            listaClientes.add(cliente);
            return ResponseEntity.ok(cliente);
        }

        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Cliente no creado.");
    }

    /**
     * Validación de duplicados en actualización (excluyendo el propio ID).
     */
    private boolean duplicadosActualizar(int id, Cliente cliente) {

        listaClientes.stream()
                .filter(n -> n.getEmail().equalsIgnoreCase(cliente.getEmail())
                        && n.getId() != id)
                .findFirst()
                .ifPresent(n -> {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                            "El email ya pertenece a otro cliente.");
                });

        return true;
    }

    /**
     * Actualización completa del cliente.
     */
    @PutMapping("/clientes/{id}")
    public ResponseEntity<Cliente> actualizarCliente(@PathVariable int id,
                                                     @RequestBody Cliente cliente) {

        if (validaciones(cliente) && duplicadosActualizar(id, cliente)) {

            Cliente c = buscarCliente(id);
            c.setNombre(cliente.getNombre());
            c.setEmail(cliente.getEmail());
            c.setEdad(cliente.getEdad());
            c.setActivo(cliente.isActivo());

            return ResponseEntity.ok(c);
        }

        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Cliente no actualizado.");
    }

    /**
     * Actualización parcial de campos del cliente.
     */
    @PatchMapping("/clientes/{id}")
    public ResponseEntity<Cliente> actualizacionParcial(@PathVariable int id,
                                                        @RequestBody Map<String, Object> cliente) {

        Cliente c = buscarPorId(id);

        if (cliente.containsKey("email")) {
            Object valor = cliente.get("email");
            if (valor instanceof String) {
                c.setEmail((String) valor);
            }
        }

        if (cliente.containsKey("edad")) {
            Object valor = cliente.get("edad");
            if (valor instanceof Number) {
                c.setEdad(((Number) valor).intValue());
            }
        }

        if (cliente.containsKey("activo")) {
            Object valor = cliente.get("activo");
            if (valor instanceof Boolean) {
                c.setActivo((Boolean) valor);
            }
        }

        return ResponseEntity.ok(c);
    }

    /**
     * Eliminación de cliente por ID.
     */
    @DeleteMapping("/clientes/{id}")
    public ResponseEntity<Cliente> borrarCliente(@PathVariable int id) {

        Cliente c = buscarPorId(id);
        listaClientes.remove(c);

        return ResponseEntity.ok(c);
    }
}
package es.pulsoft.spring_clientes.model;

public class Cliente {

    private int id;
    private String nombre;
    private String email;
    private  int edad;
    private boolean activo;


    public Cliente() {

    }

    public Cliente(int id, String nombre, String email, int edad, boolean activo) {
        this.id = id;
        this.nombre = nombre;
        this.email = email;
        this.edad = edad;
        this.activo = activo;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public int getEdad() {
        return edad;
    }

    public void setEdad(int edad) {
        this.edad = edad;
    }

    public boolean isActivo() {
        return activo;
    }

    public void setActivo(boolean activo) {
        this.activo = activo;
    }
}