Componente AlmacenExpedientes
Organizar el archivado de expedientes en una estructura de directorios jerárquica y predecible (expedientes/<año>/<mes>/<provincia>/), usando la API java.nio.file en vez de concatenar rutas a mano.
1Diseñar la jerarquía de directorios
Path representa una ruta sin tocar el disco; Files es quien hace las operaciones reales de E/S. Compón siempre con resolve(), nunca concatenando cadenas con +.
Path raiz = Path.of(directorioRaiz); // externalizada, ver paso 5
Path directorioExpediente = raiz
.resolve(String.valueOf(anio))
.resolve(String.format("%02d", mes))
.resolve(provincia);
Files.createDirectories(directorioExpediente); // crea intermedios si faltan
Path.of(...) / Paths.get(...) no requieren que el fichero exista. Todo lo que sí toca el disco (crear, comprobar, mover, borrar) vive en Files.2archivar(Expediente)
Crea los directorios intermedios que falten antes de escribir el expediente.
public void archivar(Expediente expediente) {
Path directorio = calcularDirectorio(expediente);
Files.createDirectories(directorio); // no falla si ya existen
Path fichero = directorio.resolve(expediente.getNumero() + ".json");
// serializar y escribir expediente en fichero
}
3buscar(numeroExpediente)
Sin recorrido manual del árbol: apóyate en Files.walk o Files.find con filtro, dentro de un try-with-resources (el stream mantiene descriptores de directorio abiertos).
public Optional<Path> buscar(String numeroExpediente) throws IOException {
String nombre = numeroExpediente + ".json";
try (Stream<Path> stream = Files.walk(raiz)) {
return stream
.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().equals(nombre))
.findFirst();
}
}
4mover(numeroExpediente, EstadoExpediente)
Cambiar el estado de un expediente implica moverlo de directorio con Files.move y las opciones apropiadas.
public void mover(String numeroExpediente, EstadoExpediente nuevoEstado) throws IOException {
Path origen = buscar(numeroExpediente)
.orElseThrow(() -> new NoSuchFileException(numeroExpediente));
Path destino = calcularDirectorioPorEstado(nuevoEstado).resolve(origen.getFileName());
Files.createDirectories(destino.getParent());
Files.move(origen, destino, StandardCopyOption.REPLACE_EXISTING);
}
5Raíz configurable
Externaliza el directorio raíz con @ConfigurationProperties en vez de escribir la ruta en el código: cambia por entorno sin recompilar.
@ConfigurationProperties(prefix = "app.almacenamiento")
public class AlmacenamientoProperties {
private String directorioExpedientes;
// getters y setters
}
app:
almacenamiento:
directorio-expedientes: /var/datos/expedientes
6Endpoints REST
El controlador delega en el servicio: no debe tocar Files/Path directamente.
@RestController
@RequestMapping("/expedientes")
@RequiredArgsConstructor
public class ExpedienteController {
private final AlmacenExpedientes almacen;
@GetMapping("/{numero}/archivo")
public ResponseEntity<Resource> archivo(@PathVariable String numero) {
return almacen.buscar(numero)
.map(p -> ResponseEntity.ok((Resource) new FileSystemResource(p)))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/{numero}/estado")
public ResponseEntity<Void> cambiarEstado(@PathVariable String numero,
@RequestBody EstadoExpediente nuevoEstado) {
almacen.mover(numero, nuevoEstado);
return ResponseEntity.ok().build();
}
}
Path ni Files deben aparecer en la clase @RestController: toda la E/S vive en el servicio.7Pruebas con directorio temporal
Usa Files.createTempDirectory para que cada ejecución de pruebas empiece limpia y no interfiera con otras.
@TempDir
Path directorioTemporal;
@Test
void archivaEnLaRutaEsperada() throws IOException {
AlmacenExpedientes almacen = new AlmacenExpedientes(directorioTemporal.toString());
almacen.archivar(expedienteDePrueba());
assertThat(almacen.buscar(expedienteDePrueba().getNumero())).isPresent();
}
!Problemas frecuentes
raiz + "/" + anio + "/" + mes rompe en Windows. Usa siempre resolve().Files.move no crea directorios intermedios: llama a Files.createDirectories(destino.getParent()) antes.✓Criterios de aceptación
- Las rutas se calculan con la API
Path/Files, nunca concatenando cadenas con+ archivarcrea los directorios intermedios que faltenbuscarno hace recorrido manual del árbol de directorios- El directorio raíz es configurable por propiedad, no está hardcodeado
- Las pruebas usan un directorio temporal que se limpia solo
- Los endpoints devuelven los códigos HTTP correctos (200/404)
- El controlador delega toda la E/S en el servicio
Adaptado de apartado b — Path, Paths y Files, apartado f — Excepciones y configuración y apartado h — Actividades.