1Recorrer el árbol con Files.walk

Files.walk devuelve un Stream<Path> perezoso que baja por todos los subdirectorios. Mantiene descriptores de directorio abiertos: try-with-resources es obligatorio, no opcional.

java
try (Stream<Path> stream = Files.walk(raizExpedientes)) {
    List<Path> candidatos = stream
            .filter(Files::isRegularFile)
            .filter(this::estaPrescrito)
            .collect(Collectors.toList());
}

2Aplicar la regla R4 de prescripción

3 meses para infracciones leves, 6 meses para graves/muy graves, desde la comisión sin notificación.

java
private boolean estaPrescrito(Expediente expediente) {
    Period plazo = expediente.getGravedad() == Gravedad.LEVE
            ? Period.ofMonths(3)
            : Period.ofMonths(6); // graves y muy graves
    LocalDate limite = expediente.getFechaComision().plus(plazo);
    return expediente.getNotificacion() == null
            && LocalDate.now().isAfter(limite);
}
Cuidado con la gravedadSolo prescriben los expedientes sin notificación dentro del plazo. Uno ya notificado sigue su tramitación normal (Resolución, R2, R5), no se purga aquí.

3Filtrar con PathMatcher o Stream

Si el filtro es por patrón de ruta (p.ej. solo ficheros .json), compílalo una sola vez con PathMatcher en vez de repetir la compilación glob en cada iteración del bucle.

java
PathMatcher matcher = FileSystems.getDefault()
        .getPathMatcher("glob:**/*.json");
// reutilizar matcher.matches(path) dentro del filtro del stream

4Borrar ficheros y directorios vacíos

Files.delete sobre un directorio solo funciona si está vacío: borra primero los ficheros del expediente y comprueba después si el directorio quedó vacío.

java
Files.delete(ficheroExpediente);
Path directorio = ficheroExpediente.getParent();
try (Stream<Path> contenido = Files.list(directorio)) {
    if (contenido.findAny().isEmpty()) {
        Files.delete(directorio); // ahora sí está vacío
    }
}

5Registro y control de ejecución

Deja rastro de qué se purgó antes de borrarlo, y no dejes que la purga se dispare sola.

java
@Slf4j
public class ServicioPurgaExpedientes {

    public int purgar() throws IOException {
        int purgados = 0;
        for (Expediente expediente : localizarPrescritos()) {
            log.info("Purgando expediente prescrito {} (comisión {})",
                    expediente.getNumero(), expediente.getFechaComision());
            eliminar(expediente);
            purgados++;
        }
        return purgados;
    }
}
No automática al arranqueLa purga no debe ejecutarse sola cuando arranca la aplicación (nada de @PostConstruct ni @Scheduled aquí): solo bajo demanda, desde el endpoint.

6Endpoint y pruebas

java
@PostMapping("/expedientes/purga")
public ResponseEntity<Integer> purgar() throws IOException {
    int purgados = servicioPurga.purgar();
    return ResponseEntity.ok(purgados);
}

!Problemas frecuentes

Modificar el árbol mientras se recorreBorrar dentro del propio Stream de Files.walk puede dar comportamiento indefinido. Recolecta primero la lista de candidatos, cierra el stream, y borra después.
Directorio "vacío" con ficheros ocultosEn algunos sistemas de ficheros puede haber metadatos ocultos que impidan que Files.delete vea el directorio como vacío. Compruébalo con Files.list, no asumas.

✓Criterios de aceptación

  • Usa la API java.nio.file, no java.io.File
  • Distingue correctamente el plazo por gravedad (3 vs. 6 meses)
  • Elimina los directorios que quedan vacíos tras la purga
  • Los expedientes no prescritos quedan intactos
  • Registra en log cada expediente purgado antes de borrarlo
  • El endpoint devuelve el número de expedientes purgados
  • La purga no se ejecuta automáticamente al arranque

Adaptado de apartado c — Recorrido de árboles y apartado h — Actividades.