Skip to main content

📂 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
  • 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, para DocumentDefinition, 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 mismo EntityManagerFactory.
  • 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 paquete com.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.

CampoTipoDescripción
idbigintIdentificador interno.
type_codevarcharCódigo lógico del tipo (p. ej. NOMINA, TI). Debe existir en el enum de la app.
descriptionvarcharDescripción legible del tipo.
path_templatevarcharPlantilla de ruta con placeholders (ver abajo).
activebooleanHabilita/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_definitions con active = true.

Alta y gobierno de tipos

Cualquier nuevo type_code debe seguir este flujo:

  1. Aprobación funcional (y de seguridad si aplica).
  2. Incorporación al enum de la aplicación (lista blanca de tipos válidos).
  3. Registro en la tabla document_definitions con su path_template y active = 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_code si 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 fuera false, 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);
}
}