# Día 7: APIs del Día a Día: Fechas, Texto, Ficheros, HTTP y Logging

> java.time, StringBuilder y expresiones regulares, NIO con Files y Path, HttpClient y logging con SLF4J y Logback

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 7 de 30
- Fase 1: Java Vanilla — El Núcleo del Lenguaje (17 & 21)
- Dedicación: 3.5h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/fechas-texto-ficheros-http-logging/

## Al terminar este día sabrás

- Elegir entre `Instant`, `LocalDate`, `LocalDateTime` y `ZonedDateTime` según lo que representa el dato.
- Construir y validar texto con `StringBuilder`, text blocks y expresiones regulares compiladas una vez.
- Leer y escribir ficheros con `Path` y `Files` sin fugas de recursos.
- Llamar a una API con `HttpClient` con timeouts y registrar los errores con SLF4J sin perder el stack trace.


## Conceptos

### 1. Fechas y Horas con java.time

`LocalDate`, `LocalTime` y `LocalDateTime` representan fechas sin zona horaria; `ZonedDateTime` e `Instant` representan un momento exacto. Todas son inmutables. `Duration` mide tiempo en segundos y `Period` en días, meses y años.

**Cómo funciona por dentro.**

`java.time` separa dos ideas que `Date` mezclaba: el momento exacto y su representación humana. `Instant` es un punto en la línea del tiempo (segundos y nanosegundos desde 1970 en UTC): ideal para registrar cuándo ocurrió algo. `LocalDate`, `LocalTime` y `LocalDateTime` son fechas y horas «de calendario» sin zona: un cumpleaños, la hora de apertura de una tienda. `ZonedDateTime` y `OffsetDateTime` unen una fecha local a una zona o a un desfase, y sí representan un momento exacto.

Todas las clases son inmutables y seguras entre hilos: `plusDays` devuelve un objeto nuevo. `Duration` mide intervalos en segundos (útil para timeouts), `Period` en unidades de calendario (un mes no siempre tiene los mismos días) y `DateTimeFormatter` convierte a texto y desde texto.

Para poder probar código que depende de la hora, no llames a `Instant.now()` directamente: recibe un `Clock` por constructor. En producción inyectas `Clock.systemUTC()` y en los tests `Clock.fixed(...)`, y el resultado deja de depender del momento en que se ejecuta el test.

*Instant para registrar, zona para mostrar y Clock para probar*

```java
public class Vencimientos {
    private final Clock reloj;

    public Vencimientos(Clock reloj) { this.reloj = reloj; }

    public Instant fechaLimite(Duration plazo) {
        return Instant.now(reloj).plus(plazo);                 // momento exacto, en UTC
    }

    public String paraUsuario(Instant momento, ZoneId zona) {
        return DateTimeFormatter.ofPattern("d 'de' MMMM, HH:mm", Locale.forLanguageTag("es"))
            .format(momento.atZone(zona));                     // se convierte solo al mostrar
    }
}

// En el test: tiempo fijo y resultado reproducible
var reloj = Clock.fixed(Instant.parse("2026-10-28T10:00:00Z"), ZoneOffset.UTC);
var v = new Vencimientos(reloj);

assertThat(v.fechaLimite(Duration.ofHours(48)))
    .isEqualTo(Instant.parse("2026-10-30T10:00:00Z"));
assertThat(v.paraUsuario(Instant.parse("2026-10-28T10:00:00Z"), ZoneId.of("America/Mexico_City")))
    .isEqualTo("28 de octubre, 04:00");
```

- **En una línea:** `Instant.now()`, `LocalDate.of(2026, 10, 28).plusDays(30)`, `Duration.between(inicio, fin).toMillis()`.
- **Por qué importa:** Las fechas son una fuente constante de bugs: zonas horarias, cambios de hora y formatos. `java.time` sustituye a `Date` y `Calendar`, que eran mutables y confusos.
- **Error típico:** Guardar un `LocalDateTime` para un evento global: pierdes la zona horaria. Para momentos exactos usa `Instant` u `OffsetDateTime`.
- **Java 17 frente a 21:** Idéntico en Java 17 y 21.
- **Se conecta con:** Instant en APIs REST, Zonas horarias, Clock en los tests

**Compruébalo:** ¿Qué tipo usarías para la fecha de nacimiento de un cliente?

`LocalDate`. Es una fecha de calendario que no depende de la zona horaria: alguien nacido el 3 de mayo cumple años el 3 de mayo esté donde esté.

### 2. Texto: StringBuilder, Text Blocks y Expresiones Regulares

`String` es inmutable: cada concatenación en un bucle crea un objeto nuevo. `StringBuilder` construye texto de forma eficiente. Los text blocks (`"""`) escriben texto multilínea legible. `Pattern` y `Matcher` validan y extraen datos con expresiones regulares.

**Cómo funciona por dentro.**

Un `String` no cambia nunca: `s.toUpperCase()` devuelve una cadena nueva. Eso lo hace seguro de compartir, pero concatenar en un bucle crea y copia una cadena en cada vuelta. `StringBuilder` mantiene un búfer que crece según hace falta; `String.join` y `Collectors.joining` resuelven el caso típico de unir con separador. Los text blocks (`"""`) escriben SQL, JSON o plantillas multilínea sin escapar comillas, y eliminan automáticamente la indentación común.

Una expresión regular se compila a un autómata con `Pattern.compile`, que es la parte cara. `String.matches` y `String.replaceAll` la compilan en cada llamada, así que en código que se ejecuta mucho conviene guardar el `Pattern` en un campo `static final` (es inmutable y seguro entre hilos) y crear un `Matcher` por uso. Los grupos con nombre, como `(?<anio>\d{4})`, hacen la extracción legible.

Cuidado con las expresiones que permiten retroceso catastrófico, como `(a+)+$`: con ciertas entradas tardan un tiempo exponencial y pueden bloquear un hilo. Limita la longitud de la entrada antes de aplicar la expresión.

*Patrón compilado una vez, grupos con nombre y text block*

```java
public final class Matriculas {
    // 4 cifras + 3 consonantes sin vocales, Ñ ni Q (formato español desde 2000)
    private static final Pattern MATRICULA =
        Pattern.compile("^(?<numero>\\d{4})\\s?(?<letras>[BCDFGHJKLMNPRSTVWXYZ]{3})$");

    public static Optional<String> normalizar(String entrada) {
        if (entrada == null || entrada.length() > 10) return Optional.empty();
        Matcher m = MATRICULA.matcher(entrada.strip().toUpperCase(Locale.ROOT));
        return m.matches()
            ? Optional.of(m.group("numero") + " " + m.group("letras"))
            : Optional.empty();
    }
}

String informe = """
    Informe de matrículas
    ---------------------
    Válidas:   %d
    Inválidas: %d
    """.formatted(validas, invalidas);

var csv = new StringBuilder();
for (var t : tareas) {
    csv.append(t.id()).append(';').append(t.titulo()).append('\n');
}
```

- **En una línea:** `private static final Pattern DNI = Pattern.compile("\\d{8}[A-Z]");`
- **Por qué importa:** Construir mensajes o CSV en bucles con `+` es lento, y compilar regex dentro de un bucle es un clásico problema de rendimiento.
- **Error típico:** Llamar a `String.matches()` o `Pattern.compile()` dentro de un bucle: compila la regex en cada vuelta. Compílala una vez en un campo `static final`.
- **Java 17 frente a 21:** Los text blocks son estables desde Java 15, así que están en 17 y 21. Java 21 añade `StringBuilder.repeat`.
- **Se conecta con:** Inmutabilidad de String, Validación, Rendimiento

### 3. Ficheros con NIO: Path y Files

`Path` representa una ruta y `Files` ofrece métodos para leer, escribir, copiar y recorrer ficheros: `Files.readString`, `Files.writeString`, `Files.lines`, `Files.walk`. Sustituye a la antigua clase `File`.

**Cómo funciona por dentro.**

`Path` es una ruta, exista o no (`Path.of("datos", "ventas.csv")`), y `Files` agrupa las operaciones: `exists`, `createDirectories`, `readString`, `writeString`, `lines`, `newBufferedReader`, `copy`, `move`, `delete`, `walk` y `list`. Las operaciones que fallan lanzan excepciones concretas (`NoSuchFileException`, `AccessDeniedException`) en lugar de devolver `false`, como hacía `File`.

Hay dos estilos según el tamaño. `readString` y `readAllLines` cargan todo en memoria: cómodos para ficheros pequeños. `lines` y `newBufferedReader` leen bajo demanda: el fichero puede ocupar gigas y solo tendrás en memoria la línea actual. Los métodos que devuelven un `Stream` (`lines`, `walk`, `list`) mantienen el fichero o el directorio abierto, así que van siempre en try-with-resources.

Indica siempre el charset. Desde Java 18 el predeterminado es UTF-8 en todas las plataformas, pero en Java 17 depende del sistema (en Windows suele ser windows-1252), y el mismo código puede leer mal las tildes según dónde se ejecute.

*Leer un CSV grande y escribir un informe*

```java
Path entrada = Path.of("datos", "ventas.csv");
Path salida = Path.of("informes", "ventas-por-mes.txt");

Map<YearMonth, BigDecimal> porMes;
try (Stream<String> lineas = Files.lines(entrada, StandardCharsets.UTF_8)) {
    porMes = lineas
        .skip(1)                                          // cabecera
        .map(Venta::desdeCsv)                             // Optional<Venta>
        .flatMap(Optional::stream)                        // descarta las líneas inválidas
        .collect(Collectors.groupingBy(
            v -> YearMonth.from(v.fecha()),
            TreeMap::new,
            Collectors.reducing(BigDecimal.ZERO, Venta::importe, BigDecimal::add)));
}

Files.createDirectories(salida.getParent());
String texto = porMes.entrySet().stream()
    .map(e -> e.getKey() + ": " + e.getValue())
    .collect(Collectors.joining(System.lineSeparator()));
Files.writeString(salida, texto, StandardCharsets.UTF_8);
```

- **En una línea:** `try (var lineas = Files.lines(Path.of("ventas.csv"))) { lineas.skip(1).map(Venta::parse).forEach(...); }`
- **Por qué importa:** Leer ficheros grandes línea a línea evita cargarlos enteros en memoria. Y `Files` lanza excepciones con información útil, mientras que `File` devolvía `false` sin explicar nada.
- **Error típico:** Usar `Files.lines(path)` sin try-with-resources: el stream mantiene el fichero abierto hasta que se cierra.
- **Java 17 frente a 21:** Igual en Java 17 y 21, salvo el charset por defecto: desde Java 18 es UTF-8 (JEP 400). En Java 17 depende del sistema, así que indica siempre el charset.
- **Se conecta con:** try-with-resources, Streams, Charset UTF-8

### 4. Llamadas HTTP con HttpClient

`java.net.http.HttpClient` (estándar desde Java 11) hace peticiones HTTP/1.1 y HTTP/2, síncronas (`send`) o asíncronas (`sendAsync`, que devuelve un `CompletableFuture`). Se crea una vez y se reutiliza.

**Cómo funciona por dentro.**

`HttpClient` se construye una vez con su configuración (versión de HTTP, timeout de conexión, redirecciones, executor) y se reutiliza: mantiene un pool de conexiones y es seguro entre hilos. Cada llamada se describe con un `HttpRequest` inmutable, y la respuesta se convierte con un `BodyHandler`: `ofString`, `ofInputStream`, `ofFile`, etc.

Hay dos timeouts distintos. `connectTimeout` (en el cliente) limita cuánto se espera para abrir la conexión; `timeout` (en la petición) limita cuánto se espera a la respuesta. Sin ellos, un servidor que acepta la conexión y no contesta deja el hilo bloqueado indefinidamente. Ten en cuenta también que `HttpClient` no lanza excepción con un 404 o un 500: tienes que mirar `statusCode()`.

`sendAsync` devuelve un `CompletableFuture<HttpResponse<T>>`, útil para lanzar varias peticiones en paralelo. Con virtual threads (Java 21), la versión síncrona `send` dentro de un virtual thread consigue lo mismo con código más simple. En Spring Boot, `RestClient` (síncrono) y `WebClient` (reactivo) añaden conversión a JSON, gestión de errores e integración con la observabilidad.

*Cliente reutilizable con timeouts y control del estado*

```java
public class ClienteTipos {
    private static final Logger log = LoggerFactory.getLogger(ClienteTipos.class);

    private final HttpClient http = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(2))
        .followRedirects(HttpClient.Redirect.NORMAL)
        .build();

    public Optional<String> tipoDeCambio(String moneda) {
        var peticion = HttpRequest.newBuilder(URI.create("https://api.ejemplo.com/tipos/" + moneda))
            .timeout(Duration.ofSeconds(3))
            .header("Accept", "application/json")
            .GET()
            .build();
        try {
            HttpResponse<String> respuesta = http.send(peticion, HttpResponse.BodyHandlers.ofString());
            if (respuesta.statusCode() != 200) {               // 4xx y 5xx no lanzan excepción
                log.warn("Tipo de cambio {} respondió {}", moneda, respuesta.statusCode());
                return Optional.empty();
            }
            return Optional.of(respuesta.body());
        } catch (HttpTimeoutException e) {
            log.warn("Timeout consultando {}", moneda, e);
            return Optional.empty();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();                // no te tragues la interrupción
            throw new IllegalStateException(e);
        }
    }
}
```

- **En una línea:** `HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build()`
- **Por qué importa:** Permite consumir APIs sin dependencias externas. En Spring usarás `RestClient` o `WebClient`, pero entender el cliente del JDK te enseña lo que hay debajo: timeouts, cabeceras y códigos de estado.
- **Error típico:** No configurar timeouts: si el servidor no responde, el hilo se queda esperando indefinidamente.
- **Java 17 frente a 21:** Misma API. En Java 21, `send` dentro de un virtual thread no bloquea un hilo de plataforma.
- **Se conecta con:** RestClient de Spring, Timeouts, CompletableFuture

### 5. Logging con SLF4J y Logback

SLF4J es una fachada: tu código escribe `log.info(...)` contra su API y la implementación real (Logback, la que trae Spring Boot, o Log4j2) se elige en las dependencias. Los niveles son TRACE, DEBUG, INFO, WARN y ERROR.

**Cómo funciona por dentro.**

SLF4J separa la API de logging de su implementación. Las librerías y tu código escriben contra `org.slf4j.Logger`, y en el classpath hay un único backend que hace el trabajo. Spring Boot trae Logback y lo configura con `application.properties` (`logging.level.com.miempresa=DEBUG`), o con un `logback-spring.xml` para casos avanzados.

Los marcadores `{}` retrasan el formateo hasta saber que el nivel está activo, y si el último argumento es un `Throwable`, SLF4J lo trata como la excepción del mensaje e imprime su stack trace completo. Elegir bien el nivel importa: ERROR es algo que requiere actuar, WARN algo anómalo pero tolerado, INFO los hitos de negocio y DEBUG el detalle para diagnosticar.

El MDC (Mapped Diagnostic Context) añade datos a todas las líneas de un hilo, como el id de la petición o del usuario, sin pasarlos a cada llamada. Con Spring Boot 3 y Micrometer Tracing, el id de la traza se añade al MDC automáticamente, lo que permite saltar de un log a su traza.

*Uso correcto de SLF4J y configuración en Spring Boot*

```java
public class CobrosService {
    private static final Logger log = LoggerFactory.getLogger(CobrosService.class);

    public void cobrar(Pedido pedido) {
        MDC.put("pedidoId", pedido.id().toString());   // aparece en todas las líneas de este hilo
        try {
            log.info("Cobrando pedido de {} €", pedido.total());
            pasarela.cobrar(pedido);
            log.debug("Pedido cobrado: {}", pedido);    // solo se formatea si DEBUG está activo
        } catch (PagoRechazadoException e) {
            log.warn("Pago rechazado: {}", e.motivo()); // anómalo pero esperado
        } catch (RuntimeException e) {
            log.error("Fallo inesperado cobrando el pedido {}", pedido.id(), e);  // con stack trace
            throw e;
        } finally {
            MDC.remove("pedidoId");
        }
    }
}

// application.properties
// logging.level.root=INFO
// logging.level.com.miempresa.cobros=DEBUG
// logging.pattern.console=%d{HH:mm:ss} %-5level [%X{pedidoId}] %logger{36} - %msg%n
```

- **En una línea:** `log.error("No se pudo cobrar el pedido {}", pedidoId, e);`
- **Por qué importa:** Los logs son tu principal herramienta para entender un fallo en producción. Bien hechos son baratos; mal hechos llenan discos o esconden el error.
- **Error típico:** Concatenar en el mensaje y registrar `e.getMessage()` en vez de la excepción: pierdes el stack trace. Usa marcadores `{}` y pasa la excepción como último argumento.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Observabilidad, Logs estructurados, Niveles por paquete


## Sesión de 90 minutos

### Bloque 1: Fechas sin Sorpresas (0 - 25 min)

- **Qué estudiar:** `Instant`, `LocalDate`, `ZonedDateTime`, `Duration`, `Period` y `Clock`.
- **Pregunta a responder:** ¿Qué tipo usarías para una fecha de nacimiento y cuál para la hora de un pago?
- **Entregable:** Clase `CalculadoraVencimientos` que recibe un `Clock`, con tests que fijan el tiempo.

### Bloque 2: Texto y Expresiones Regulares (25 - 45 min)

- **Qué estudiar:** `StringBuilder`, text blocks, `Pattern` y grupos con nombre.
- **Pregunta a responder:** ¿Por qué hay que compilar la regex una sola vez?
- **Entregable:** Validador de matrículas españolas con `Pattern` y tests de casos válidos e inválidos.

### Bloque 3: Ficheros, HTTP y Logs (45 - 80 min)

- **Qué estudiar:** `Files.lines`, `HttpClient` con timeouts y SLF4J con marcadores.
- **Pregunta a responder:** ¿Cómo registrar un error sin perder el stack trace?
- **Entregable:** Programa que lee un CSV, llama a una API pública por cada fila con `HttpClient` y registra los errores con SLF4J.

### Cierre de Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso de la rúbrica del tutor.
- **Pregunta a responder:** Explicar sin mirar la diferencia entre `Instant` y `LocalDateTime`.
- **Entregable:** Lista de las tres APIs del JDK que más vas a usar en tu proyecto y para qué.


## Explícalo desde cero

### La diferencia entre LocalDateTime e Instant

- **Qué entender:** `LocalDateTime` es lo que marca un reloj de pared sin saber dónde está; `Instant` es un punto exacto en la línea del tiempo, igual en todo el mundo. Para registrar cuándo pasó algo, `Instant`. Para una cita en el calendario de una persona, fecha local más su zona.
- **Qué suele explicarse mal:** Decir que `LocalDateTime` es «la hora local del servidor». No tiene zona: es solo una fecha y una hora sin contexto.
- **Ejemplo sencillo:** «Reunión a las 10:00» no basta si un asistente está en Madrid y otro en Lima. Hace falta saber de qué 10:00 hablas.
- **Señal de alerta:** Si guardas fechas de eventos con `LocalDateTime` y tu aplicación tiene usuarios en varias zonas horarias.


## Preguntas

### Nivel 1 (Básico): ¿Por qué concatenar cadenas con `+` dentro de un bucle de 100.000 vueltas es mala idea?

Porque `String` es inmutable: cada `+` crea una cadena nueva y copia la anterior, con un coste que crece de forma cuadrática. `StringBuilder` reutiliza un búfer interno. Fuera de bucles, el compilador ya optimiza las concatenaciones simples.

**Respuesta razonada:** Porque cada `+` crea una cadena nueva y copia en ella todos los caracteres anteriores. En la vuelta 1.000 copia 1.000 trozos y en la 100.000 copia 100.000: el coste total crece con el cuadrado del número de vueltas y genera millones de objetos temporales para el recolector.

`StringBuilder` añade al final de un búfer que duplica su capacidad cuando se llena, así que el coste total es lineal. Fuera de bucles no hace falta: el compilador ya traduce `"a" + b + "c"` a una concatenación optimizada (con `invokedynamic` desde Java 9).

### Nivel 2 (Intermedio): Un servicio llama a una API externa con `HttpClient` y a veces se queda colgado minutos. ¿Qué falta?

Timeouts. `connectTimeout` en el cliente limita el tiempo de conexión y `timeout` en la petición (`HttpRequest.newBuilder().timeout(...)`) el tiempo total de respuesta. Sin ellos el hilo espera indefinidamente. En producción se combina con reintentos y circuit breaker.

**Respuesta razonada:** Le faltan timeouts. Sin `connectTimeout`, si el servidor no acepta conexiones el hilo espera lo que decida el sistema operativo; sin `timeout` en la petición, si el servidor acepta y no responde, espera indefinidamente. Cada petición colgada ocupa un hilo, y con suficientes el servicio entero deja de responder.

La solución completa: timeouts en los dos niveles, reintentos acotados solo para errores transitorios y operaciones idempotentes, y un circuit breaker que deje de llamar a la API si falla de forma continuada. Y registrar cada timeout para saber que está pasando.

### Nivel 3 (Entrevista Senior): ¿Qué problema tiene `log.debug("Pedido: " + pedido)` en un servicio con mucho tráfico, aunque el nivel DEBUG esté desactivado?

La concatenación y el `toString()` se ejecutan siempre, antes de que el logger compruebe el nivel. Con marcadores (`log.debug("Pedido: {}", pedido)`) el mensaje solo se formatea si el nivel está activo. Si el argumento es caro de calcular, SLF4J 2 permite pasarlo como `Supplier` con la API fluida (`log.atDebug().addArgument(() -> calcular())`).

**Respuesta razonada:** El argumento de `log.debug` se evalúa antes de llamar al método: Java construye la cadena y llama al `toString()` del pedido (que puede recorrer colecciones o disparar relaciones LAZY) aunque el logger la descarte al ver que DEBUG está desactivado. Con miles de peticiones por segundo es CPU y basura desperdiciadas.

Con `log.debug("Pedido: {}", pedido)` solo se pasa la referencia y el formateo ocurre únicamente si el nivel está activo. Si calcular el argumento es caro en sí mismo, se envuelve en un `if (log.isDebugEnabled())` o se usa la API fluida de SLF4J 2 con un `Supplier`.


## Reto del tutor

**Reto:** Un compañero guarda en la base de datos la fecha de cada pedido con `LocalDateTime.now()`. La aplicación se va a desplegar en servidores de Irlanda y tiene clientes en México. Evalúa la decisión.

- **Lo que está bien:** Usa `java.time` en lugar de `Date`.
- **Respuesta superficial:** Decir «hay que usar UTC» sin explicar dónde ni cómo.
- **Lo que falta de fondo:** `LocalDateTime.now()` toma la zona del servidor y la descarta: el dato no indica en qué zona se generó. Si cambian los servidores o su configuración, los datos dejan de ser comparables, y en los cambios de hora puede haber horas repetidas.
- **Respuesta senior:** Para el momento en que ocurre algo se guarda un `Instant` (o un `OffsetDateTime` en UTC, en una columna `timestamp with time zone`) y se convierte a la zona del usuario solo al mostrarlo. Además se inyecta un `Clock` en vez de llamar a `now()` directamente, para poder fijar el tiempo en los tests.


## Katas

### Kata 7.1: Informe de ventas desde CSV

Lee `ventas.csv` con `Files.lines`, descarta las líneas cuyo código no cumpla `^V-\d{6}$`, agrupa el total por mes con `YearMonth` y escribe el resultado en `informe.txt` con `Files.writeString`. Registra con SLF4J cuántas líneas se descartaron.

### Kata 7.2: Cliente HTTP con timeouts

Con `HttpClient`, consulta `https://httpbin.org/delay/5` con un timeout de 2 segundos. Captura la `HttpTimeoutException`, regístrala con su stack trace y devuelve un resultado vacío en lugar de propagar el error.


## Reglas de oro

- Para registrar cuándo pasó algo, `Instant`; para mostrarlo, conviértelo a la zona del usuario.
- Compila las expresiones regulares una vez, en un campo `static final`.
- Todo `HttpClient` lleva timeouts.
- En los logs, marcadores `{}` y la excepción como último argumento.


## Java 17 frente a Java 21

| Característica | Java 17 | Java 21 |
| --- | --- | --- |
| Text blocks | Sí (desde Java 15) | Sí |
| Charset por defecto | Depende del sistema operativo | UTF-8 siempre (JEP 400, desde Java 18) |
| StringBuilder.repeat | No | Sí |


## Proyecto guiado «Pedidos»: Importar el catálogo y registrar lo que pasa

Cargar datos reales desde un fichero, trabajar con fechas sin ambigüedad y dejar rastro en los logs.

**Tareas:**

- Importa el catálogo desde `catalogo.csv` con `Files.lines`, descartando las líneas cuyo sku no cumpla un `Pattern`.
- Guarda la fecha de creación de cada pedido como `Instant` obtenido de un `Clock` inyectado.
- Registra con SLF4J cada pedido confirmado (INFO) y cada línea descartada del CSV (WARN).

**Está listo cuando:**

- Los tests fijan el reloj con `Clock.fixed` y no dependen de la hora a la que se ejecutan.
- Un CSV con líneas erróneas se importa igual y el log dice cuántas se descartaron.


## Fuentes oficiales

- [Javadoc de java.time](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/package-summary.html)
- [JEP 378: Text Blocks](https://openjdk.org/jeps/378)
- [Javadoc de java.nio.file.Files](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html)
- [Javadoc de HttpClient](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpClient.html)
- [Manual de SLF4J](https://www.slf4j.org/manual.html)

