📂 Alpura Back Java Library — Gestión de Archivos (Alfresco + Oracle Object Storage)
Librería para integrar en microservicios Java 21 + Spring Boot 3.4.x con Alfresco (privado) y Oracle Cloud Object Storage (OCI) (público). Provee un gateway unificado para subir, descargar, listar, actualizar y eliminar documentos.
🛠 Requisitos
- Java: 21 (Temurin recomendado)
- Spring Boot: 3.4.x
- Alfresco REST API v1 habilitado (para contenidos privados)
- OCI Tenancy con Object Storage (para contenidos públicos)
📥 Instalación
Maven (app consumidora)
<dependency>
<groupId>com.alpura.file</groupId>
<artifactId>alpura-back-java-lib-file</artifactId>
<version>1.8</version> <!-- usa la última publicada -->
</dependency>
Nota: La librería ya incluye las dependencias del SDK de OCI (incluido el httpclient Jersey3). En condiciones normales no necesitas agregarlas manualmente en tu app.
Gradle (alternativa)
implementation "com.alpura.file:alpura-back-java-lib-file:1.8"
Escaneo de componentes
Asegúrate de escanear el paquete de la librería:
@SpringBootApplication
@ComponentScan({
"com.alpura.file", // ⬅ obligatorio para encontrar los @Service de la lib
"com.alpura.java.arch" // tus/otros paquetes
})
public class ExApplication {
public static void main(String[] args) {
SpringApplication.run(ExApplication.class, args);
}
}
🚀 ¿Qué hace la librería?
- Expone un gateway (
IFileGateway) que decide el backend:- Oracle Object Storage (OCI) si
isPublic = true - Alfresco si
isPublic = false
- Oracle Object Storage (OCI) si
- Resuelve rutas lógicas a partir de plantillas y placeholders (p. ej.
clientes/{clienteId}/facturas/{year}/{month}/). - Implementa operaciones: upload, download, list, delete, update.
- (Opcional) HealthCheck de Oracle (HEAD bucket) si se habilita.
📦 Integración de la librería de archivos en el EMF primario
En tu configuración estás incorporando repositorios y entidades que viven dentro de la librería de archivos. Los dos puntos clave son:
1) Escaneo de repositorios (Spring Data)
@EnableJpaRepositories(
basePackages = {
"com.alpura.java.arch.persistence",
"com.alpura.file.repository" // 👈 repos de la librería
},
entityManagerFactoryRef = "primaryPostgresEMF",
transactionManagerRef = "primaryPostgresTrxManager"
)
public class PrimaryDataConfig {}
com.alpura.file.repository: habilita que Spring detecte los Repository que provee la librería (por ejemplo, paraDocumentDefinition, etc.).- Si omites este paquete, no se crearán los beans de esos repositorios y tendrás errores de inyección.
2) Escaneo de entidades (JPA)
public LocalContainerEntityManagerFactoryBean primaryPostgresEntityManagerFactory(
@Qualifier("primaryPostgresDataSource") DataSource primaryPostgresDataSource) {
LocalContainerEntityManagerFactoryBean localContainer = new LocalContainerEntityManagerFactoryBean();
localContainer.setDataSource(primaryPostgresDataSource);
localContainer.setPersistenceUnitName("primaryPostgres");
localContainer.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Map<String, Object> props = new HashMap<>();
props.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
props.put("hibernate.hbm2ddl.auto", "update");
props.put("hibernate.show_sql", true);
props.put("hibernate.format_sql", true);
localContainer.setJpaPropertyMap(props);
localContainer.setPackagesToScan(
"com.alpura.model.entity",
"com.alpura.file.entity" // 👈 entidades de la librería
);
return localContainer;
}
com.alpura.file.entity: permite que JPA registre las Entities definidas en la librería para el mismoEntityManagerFactory.- Si no se incluye, Hibernate no mapeará esas tablas y fallarán consultas/relaciones que las usen.
📌 Requisitos y tips
- Asegúrate de tener la dependencia de la librería en tu
pom.xml/build (el paquetecom.alpura.file.*debe estar en el classpath). - Mantén compatibles las versiones de la librería y de Hibernate/Spring Boot para evitar errores de mapeo.
- Evita duplicar rutas de escaneo (no mezclar esos paquetes en otros EMF) para no registrar entidades/repositorios dos veces.
⚙️ Configuración (YAML)
1) Alfresco (privado)
alfresco:
host: https://alfresco.tuempresa.com
api-base-path: alfresco/api/-default-/public/alfresco/versions/1
username: usuario_servicio
password: "********"
2) Oracle Object Storage (público)
alpura:
oracle:
namespace: idhnamyewasy # ← requerido
bucket: ALOCIAPIFACELDEV # ← requerido
region: us-ashburn-1 # ← requerido (evita warnings del SDK)
auth:
tenancy-ocid: "ocid1.tenancy.oc1...."
user-ocid: "ocid1.user.oc1...."
fingerprint: "aa:bb:cc:dd:..."
private-key-path: "C:/seguro/oci_api_key.pem" # ruta válida al PEM (NO embebas la llave en la propiedad)
passphrase: "" # si tu llave la tiene (opcional)
🗄️ Base de datos
Este servicio utiliza PostgreSQL. Antes de desplegar en cualquier entorno, define y valida la ubicación del esquema/DB que alojará las tablas.
Tabla: ALP_TI_FH_DOCUMENT_DEFINITION
Contiene la definición de cada tipo de documento y cómo construir su ruta de almacenamiento.
| Campo | Tipo | Descripción |
|---|---|---|
id | bigint | Identificador interno. |
type_code | varchar | Código lógico del tipo (p. ej. NOMINA, TI). Debe existir en el enum de la app. |
description | varchar | Descripción legible del tipo. |
path_template | varchar | Plantilla de ruta con placeholders (ver abajo). |
active | boolean | Habilita/inhabilita el tipo para uso en la aplicación. |
Placeholders soportados en path_template
{idEmpleado}— Identificador del empleado.{year}— Año en 4 dígitos.{month}— Mes en 2 dígitos (01–12).
Ejemplos de path_template (solo ilustrativos):
/RecursosHumanos/Empleado/{idEmpleado}/{year}/{month}
/Tecnologia/Empleado/{idEmpleado}/{year}/{month}
[!IMPORTANT] Las rutas anteriores son referenciales. La ruta final debe ser revisada y preautorizada por el responsable funcional y/o seguridad antes de usarse en producción, y registrada en
document_definitionsconactive = true.
Alta y gobierno de tipos
Cualquier nuevo type_code debe seguir este flujo:
- Aprobación funcional (y de seguridad si aplica).
- Incorporación al enum de la aplicación (lista blanca de tipos válidos).
- Registro en la tabla
document_definitionscon supath_templateyactive = true.
Sin este proceso, el microservicio no resolverá la ruta ni aceptará el tipo.
Carga inicial (SQL de ejemplo)
INSERT INTO document_definitions (id, type_code, description, path_template, active) VALUES
(1, 'NOMINA', 'Documentos de nómina.', '/RecursosHumanos/Empleado/{idEmpleado}/{year}/{month}', true),
(2, 'TI', 'Documentos de tecnología.', '/Tecnologia/Empleado/{idEmpleado}/{year}/{month}', true);
Enum en la aplicación (ejemplo)
public enum DocumentType {
NOMINA,
TI
// Agrega aquí nuevos códigos tras su aprobación y registro en BD
}
Buenas prácticas
- Versiona cambios estructurales y datos semilla con Flyway/Liquibase.
- Valida que todos los placeholders requeridos estén presentes al componer la ruta.
- Considera índices sobre
type_codesi la consulta por tipo es frecuente. - Mantén la coherencia entre enum (app) y tabla (BD) para evitar rechazos en tiempo de ejecución.
🧩 Objetos clave (request/response)
Lo mínimo que necesitas para usar el gateway.
ManagedDocumentMessenger (subir/actualizar)
public class ManagedDocumentMessenger {
MultipartFile file; // archivo
DocumentType type; // p. ej. NOMINA
// Los placeholders corresponden a la ruta de archivo previamente revisada y dada de alta en BD para ese type
Map<String, String> placeholders; // { "clienteId":"001", "year":"2025", "month":"09" }
Boolean isPublic; // true=Oracle, false=Alfresco
}
DocumentDownloadRequest (descargar/eliminar)
public class DocumentDownloadRequest {
DocumentType type;
Map<String, String> placeholders;
String fileName; // nombre exacto con extensión
Boolean isPublic; // true=Oracle, false=Alfresco
}
DocumentListRequest (listar)
public class DocumentListRequest {
DocumentType type;
Map<String, String> placeholders;
Boolean isPublic;
Integer skip = 0;
Integer max = 100;
}
🔁 Respuestas y manejo de descarga
1) Respuesta al subir (upload)
Cuando subes un archivo, el gateway devuelve un FileEntryVO con la información mínima del objeto. Ejemplo:
{
"id": null,
"name": "crema.webp",
"nodeType": null,
"isFolder": false,
"isFile": true,
"aspectNames": null,
"createdAt": null,
"modifiedAt": null,
"createdByUser": null,
"modifiedByUser": null,
"parentId": null,
"logicalPath": "/RecursosHumanos/Empleado/12345/2024/09/crema.webp",
"content": null,
"public": true
}
logicalPath: ruta de negocio final (prefijo/carpeta + nombre).public = true: indica que quedó almacenado en Oracle Object Storage; si fuerafalse, sería Alfresco.
2) Descarga con nombre de archivo (desde el controlador)
La descarga debe gestionarse en tu controlador para entregar el binario con el nombre de archivo correcto. El gateway te regresa los bytes (ByteArrayResource) y tú defines los headers HTTP:
@PostMapping("/download")
public ResponseEntity<ByteArrayResource> download(@ModelAttribute DocumentDownloadRequest rq) {
// 1) Descarga los bytes desde el gateway
ByteArrayResource body = gateway.downloadDocument(rq);
// 2) Usa el nombre del archivo que viene en el request (rq.getFileName())
String filename = rq.getFileName();
// 3) Retorna con Content-Disposition para forzar el nombre de descarga
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(body.contentLength())
.body(body);
}
Tip: si conoces el content-type real del objeto, cámbialo por el MIME correcto.
🔌 Endpoints de ejemplo (opcionales)
Tú decides si expones endpoints HTTP. Un controlador de referencia para Oracle:
@RestController
@RequestMapping("/oracle-storage")
@RequiredArgsConstructor
public class OracleController {
private final IFileGateway gateway;
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public FileEntryVO upload(@ModelAttribute ManagedDocumentMessenger msg) {
return gateway.saveDocument(msg);
}
@PostMapping("/download")
public ResponseEntity<ByteArrayResource> download(@ModelAttribute DocumentDownloadRequest rq) {
return ResponseEntity.ok(gateway.downloadDocument(rq));
}
@DeleteMapping("/delete")Pero
public ResponseEntity<Void> delete(@ModelAttribute DocumentDownloadRequest rq) {
gateway.deleteDocument(rq);
return ResponseEntity.noContent().build();
}
@PutMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public FileEntryVO update(@ModelAttribute ManagedDocumentMessenger msg, @RequestParam String oldFileName) {
return gateway.updateDocument(msg, oldFileName);
}
@GetMapping("/list")
public ListResponseVO<FileEntryVO> list(@ModelAttribute DocumentListRequest rq) {
return gateway.listDocuments(rq);
}
}