Nach der Transkription eines Dokuments werden referenzierte externe
Dokumente (AGB, Verhaltenskodex) erkannt, sicher heruntergeladen und ihr
Text dem Hauptdokument fuer die "Summen"-Bewertung angehaengt (Option D).
Die Referenzen werden zusaetzlich als eigene, herunterladbare Zeilen
gespeichert.
- DB: dc_project_documents um parent_document_id, is_reference, source_url
erweitert (DC_REFERENCES_ALTER.sql)
- ORDS: Dokumentenliste um die Felder erweitert; neue Endpunkte
POST /documents/:id/references und DELETE /projects/:id/references;
q-Quote-Parserfehler im ai-costs-Handler ('[]') behoben
- Backend: ReferenceDownloadService (SSRF-sicher via DNS-/Public-IP-
Validierung, Redirect-Pruefung, Groessen-/Timeout-Limits, jsoup fuer HTML),
EvaluationService.extractReferenceUrls (LLM-basierte URL-Erkennung),
Orchestrierung (Discovery in Phase A, Referenz-Skip + combinedText in Phase B)
- Config: dc.references.* in application.properties
Verifiziert an Projekt 144: AGB-PDF/-HTML heruntergeladen, verknuepft,
"Summen"-Bewertung zitiert AGB-Klauseln aus dem Referenzdokument.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
730 lines
27 KiB
MySQL
730 lines
27 KiB
MySQL
-- =============================================================================
|
||
-- DC_BACKEND_PKG
|
||
-- PL/SQL-Schnittstelle zum Quarkus dc-backend (Kubernetes Service dc-backend:8090)
|
||
--
|
||
-- Voraussetzungen:
|
||
-- - APEX_WEB_SERVICE-Berechtigung für das Schema:
|
||
-- GRANT EXECUTE ON APEX_240100.APEX_WEB_SERVICE TO <schema>; -- APEX-Version anpassen
|
||
-- - ACL für ausgehende HTTP-Verbindung im selben Namespace:
|
||
-- DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
|
||
-- host => 'dc-backend', lower_port => 8090, upper_port => 8090,
|
||
-- ace => xs$ace_type(privilege_list => xs$name_list('connect','resolve'),
|
||
-- principal_name => '<schema>', principal_type => xs_acl.ptype_db));
|
||
--
|
||
-- Deployment:
|
||
-- sqlplus user/pass@db @"Scripts/DC_BACKEND_PKG.sql"
|
||
-- =============================================================================
|
||
|
||
|
||
-- =============================================================================
|
||
-- Package Spec
|
||
-- =============================================================================
|
||
CREATE OR REPLACE PACKAGE DC_BACKEND_PKG AS
|
||
|
||
-- Basis-URL des dc-backend Kubernetes Service
|
||
c_base_url CONSTANT VARCHAR2(200) := 'http://dc-backend:8090';
|
||
|
||
-- API-Key (X-API-KEY Header).
|
||
-- Leer = kein Auth (Dev-Modus).
|
||
-- Zur Laufzeit setzen: DC_BACKEND_PKG.g_api_key := 'mein-key';
|
||
g_api_key VARCHAR2(500) := '';
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Startet die KI-gestützte Dokumentenprüfung für ein Projekt (asynchron).
|
||
-- Das Projekt muss im Status PENDING sein.
|
||
-- POST /check/{p_project_id} → 202 Accepted
|
||
--
|
||
-- p_project_id ID des dc_projects-Eintrags
|
||
-- p_status_code HTTP-Statuscode der Antwort (202 = OK, 409 = nicht PENDING)
|
||
-- p_response JSON-Antwort des Backend
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE start_check(
|
||
p_project_id IN NUMBER,
|
||
p_status_code OUT NUMBER,
|
||
p_response OUT VARCHAR2
|
||
);
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Health-Check: prüft ob der dc-backend Service erreichbar ist.
|
||
-- GET /check/health → 200 UP
|
||
--
|
||
-- Rückgabe: TRUE = Service läuft, FALSE = nicht erreichbar
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION health_check RETURN BOOLEAN;
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Setzt Projekt auf PENDING zurück – eigene Transaktion (AUTONOMOUS COMMIT).
|
||
-- dc_projects.status → PENDING, report_markdown → NULL
|
||
-- dc_project_documents.original_text/translated_text → NULL, progress → 0
|
||
-- dc_results löschen
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE reset_project(
|
||
p_project_id IN NUMBER
|
||
);
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Startet die Prüfung und wirft eine Exception wenn nicht 202 zurückkommt.
|
||
-- Convenience-Wrapper für direkte APEX-Button-Aufrufe.
|
||
--
|
||
-- p_reset TRUE = Projekt vor dem Start zurücksetzen (eigener COMMIT):
|
||
-- dc_projects.status → PENDING, report_markdown → NULL
|
||
-- dc_project_documents.original_text/translated_text → NULL, progress → 0
|
||
-- dc_results löschen
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE start_check_or_raise(
|
||
p_project_id IN NUMBER,
|
||
p_reset IN BOOLEAN DEFAULT FALSE
|
||
);
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Fortschritts-Record
|
||
-- -------------------------------------------------------------------------
|
||
TYPE t_project_status IS RECORD (
|
||
status dc_projects.status%TYPE,
|
||
processing_progress dc_projects.progress%TYPE,
|
||
is_completed BOOLEAN,
|
||
is_running BOOLEAN,
|
||
is_pending BOOLEAN
|
||
);
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Liest Status und Fortschritt eines Projekts direkt aus der DB.
|
||
-- Kein REST-Aufruf – sofort und transaktionssicher.
|
||
--
|
||
-- Beispiel:
|
||
-- l_s := DC_BACKEND_PKG.get_status(1);
|
||
-- DBMS_OUTPUT.PUT_LINE(l_s.status || ' ' || l_s.processing_progress || '%');
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION get_status(
|
||
p_project_id IN NUMBER
|
||
) RETURN t_project_status;
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Wartet (polling) bis das Projekt COMPLETED oder ein Timeout erreicht ist.
|
||
-- Wirft eine Exception bei Timeout oder wenn das Projekt nicht gefunden wird.
|
||
--
|
||
-- p_timeout_sec Max. Wartezeit in Sekunden (Default: 1800 = 30 Min.)
|
||
-- p_interval_sec Polling-Intervall in Sekunden (Default: 5)
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE wait_for_completion(
|
||
p_project_id IN NUMBER,
|
||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||
p_interval_sec IN NUMBER DEFAULT 5
|
||
);
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Konvertiert einen Markdown-CLOB zu DOCX oder PDF.
|
||
-- POST /export/markdown → BLOB (application/pdf oder .docx)
|
||
--
|
||
-- p_markdown Markdown-Text (z.B. dc_projects.report_markdown)
|
||
-- p_format 'PDF' (Standard) oder 'DOCX'
|
||
-- Rückgabe BLOB mit dem fertigen Dokument
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION convert_markdown(
|
||
p_markdown IN CLOB,
|
||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||
) RETURN BLOB;
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Convenience: liest report_markdown des Projekts und konvertiert zu PDF/DOCX.
|
||
--
|
||
-- Beispiel:
|
||
-- v_blob := DC_BACKEND_PKG.project_report(1, 'PDF');
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION project_report(
|
||
p_project_id IN NUMBER,
|
||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||
) RETURN BLOB;
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Generiert den Markdown-Prüfbericht für ein Projekt neu und speichert ihn
|
||
-- in dc_projects.report_markdown. Ruft intern DC_UTILS_PKG.generate_report auf.
|
||
--
|
||
-- Beispiel:
|
||
-- DC_BACKEND_PKG.regenerate_report(1);
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE regenerate_report(
|
||
p_project_id IN NUMBER
|
||
);
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Startet die asynchrone Katalog-Generierung aus einem Regelwerk-Dokument.
|
||
-- Gibt sofort zurück (202 Accepted-Muster) – die KI läuft im Hintergrund.
|
||
--
|
||
-- p_file BLOB des Regelwerks (PDF, DOCX, ODT, TXT)
|
||
-- p_mime_type MIME-Typ, z.B. 'application/pdf'
|
||
-- p_filename Dateiname (wird im multipart-Part übergeben)
|
||
-- p_catalog_name Name des anzulegenden Katalogs (z.B. 'ADSp 2017')
|
||
-- p_document_type_id ID des Dokumenttyps (dc_document_types.id)
|
||
-- p_description Optionale Beschreibung
|
||
-- Rückgabe ID des Katalog-Placeholders (Status: GENERATING)
|
||
--
|
||
-- Beispiel (mit Warten auf Abschluss):
|
||
-- DECLARE
|
||
-- l_blob BLOB;
|
||
-- l_id NUMBER;
|
||
-- BEGIN
|
||
-- SELECT file_blob INTO l_blob FROM dc_reference_documents WHERE id = 1;
|
||
-- l_id := DC_BACKEND_PKG.generate_catalog(
|
||
-- p_file => l_blob,
|
||
-- p_mime_type => 'application/pdf',
|
||
-- p_filename => 'ADSp-2017.pdf',
|
||
-- p_catalog_name => 'ADSp 2017',
|
||
-- p_document_type_id => 1,
|
||
-- p_description => 'Allgemeine Deutsche Spediteurbedingungen 2017');
|
||
-- DBMS_OUTPUT.PUT_LINE('Katalog ID: ' || l_id || ' (läuft im Hintergrund)');
|
||
-- DC_BACKEND_PKG.wait_for_catalog(l_id); -- optional, wartet bis READY
|
||
-- DBMS_OUTPUT.PUT_LINE('Katalog fertig!');
|
||
-- END;
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION generate_catalog(
|
||
p_file IN BLOB,
|
||
p_mime_type IN VARCHAR2 DEFAULT 'application/pdf',
|
||
p_filename IN VARCHAR2 DEFAULT 'dokument.pdf',
|
||
p_catalog_name IN VARCHAR2,
|
||
p_document_type_id IN NUMBER,
|
||
p_description IN VARCHAR2 DEFAULT NULL
|
||
) RETURN NUMBER;
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Liest den Generierungsstatus direkt aus der DB (kein REST-Aufruf).
|
||
-- Rückgabe: 'GENERATING', 'READY' oder 'ERROR'
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION get_catalog_status(
|
||
p_catalog_id IN NUMBER
|
||
) RETURN VARCHAR2;
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Wartet (polling) bis der Katalog den Status READY erreicht hat.
|
||
-- Wirft eine Exception bei Timeout oder wenn der Katalog ERROR meldet.
|
||
--
|
||
-- p_timeout_sec Max. Wartezeit in Sekunden (Default: 1800 = 30 Min.)
|
||
-- p_interval_sec Polling-Intervall in Sekunden (Default: 10)
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE wait_for_catalog(
|
||
p_catalog_id IN NUMBER,
|
||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||
p_interval_sec IN NUMBER DEFAULT 10
|
||
);
|
||
|
||
END DC_BACKEND_PKG;
|
||
/
|
||
|
||
|
||
-- =============================================================================
|
||
-- Package Body
|
||
-- =============================================================================
|
||
CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Setzt Projekt auf PENDING zurück – eigene Transaktion (AUTONOMOUS).
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE reset_project(p_project_id IN NUMBER) IS
|
||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||
BEGIN
|
||
UPDATE dc_projects
|
||
SET status = 'PENDING',
|
||
report_markdown = NULL
|
||
WHERE id = p_project_id;
|
||
|
||
UPDATE dc_project_documents
|
||
SET original_text = NULL,
|
||
translated_text = NULL,
|
||
progress = 0,
|
||
status = 'PENDING'
|
||
WHERE project_id = p_project_id;
|
||
|
||
DELETE FROM dc_results
|
||
WHERE project_id = p_project_id;
|
||
|
||
COMMIT;
|
||
END reset_project;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Setzt gemeinsame Request-Header (Content-Type + optional API-Key)
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE set_headers IS
|
||
l_idx PLS_INTEGER := 1;
|
||
BEGIN
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;
|
||
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).NAME := 'Content-Type';
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).VALUE := 'application/json';
|
||
l_idx := l_idx + 1;
|
||
|
||
IF g_api_key IS NOT NULL AND LENGTH(TRIM(g_api_key)) > 0 THEN
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).NAME := 'X-API-KEY';
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).VALUE := g_api_key;
|
||
END IF;
|
||
END set_headers;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE start_check(
|
||
p_project_id IN NUMBER,
|
||
p_status_code OUT NUMBER,
|
||
p_response OUT VARCHAR2
|
||
) IS
|
||
l_url VARCHAR2(500);
|
||
l_response CLOB;
|
||
BEGIN
|
||
l_url := c_base_url || '/check/' || TO_CHAR(p_project_id);
|
||
|
||
set_headers();
|
||
|
||
l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
|
||
p_url => l_url,
|
||
p_http_method => 'POST',
|
||
p_body => '{}' -- leerer Body, projectId ist im Pfad
|
||
);
|
||
|
||
p_status_code := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||
p_response := SUBSTR(l_response, 1, 4000);
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
p_status_code := -1;
|
||
p_response := 'Verbindungsfehler: ' || SQLERRM;
|
||
END start_check;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION health_check RETURN BOOLEAN IS
|
||
l_url VARCHAR2(500);
|
||
l_response CLOB;
|
||
l_status NUMBER;
|
||
BEGIN
|
||
l_url := c_base_url || '/check/health';
|
||
|
||
set_headers();
|
||
|
||
l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
|
||
p_url => l_url,
|
||
p_http_method => 'GET'
|
||
);
|
||
|
||
l_status := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||
RETURN l_status = 200;
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
RETURN FALSE;
|
||
END health_check;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE start_check_or_raise(
|
||
p_project_id IN NUMBER,
|
||
p_reset IN BOOLEAN DEFAULT FALSE
|
||
) IS
|
||
l_status NUMBER;
|
||
l_response VARCHAR2(4000);
|
||
BEGIN
|
||
IF p_reset THEN
|
||
reset_project(p_project_id);
|
||
END IF;
|
||
|
||
start_check(
|
||
p_project_id => p_project_id,
|
||
p_status_code => l_status,
|
||
p_response => l_response
|
||
);
|
||
|
||
IF l_status = 202 THEN
|
||
-- OK: Verarbeitung wurde gestartet
|
||
NULL;
|
||
ELSIF l_status = 409 THEN
|
||
RAISE_APPLICATION_ERROR(-20100,
|
||
'Projekt ' || p_project_id || ' ist nicht im Status PENDING.');
|
||
ELSIF l_status = 401 THEN
|
||
RAISE_APPLICATION_ERROR(-20101,
|
||
'Ungültiger API-Key für dc-backend.');
|
||
ELSIF l_status = -1 THEN
|
||
RAISE_APPLICATION_ERROR(-20102,
|
||
'dc-backend nicht erreichbar: ' || l_response);
|
||
ELSE
|
||
RAISE_APPLICATION_ERROR(-20103,
|
||
'Unerwarteter HTTP-Status ' || l_status || ': ' || l_response);
|
||
END IF;
|
||
END start_check_or_raise;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION get_status(
|
||
p_project_id IN NUMBER
|
||
) RETURN t_project_status IS
|
||
l_rec t_project_status;
|
||
BEGIN
|
||
SELECT status, progress
|
||
INTO l_rec.status, l_rec.processing_progress
|
||
FROM dc_projects
|
||
WHERE id = p_project_id;
|
||
|
||
l_rec.is_completed := l_rec.status = 'COMPLETED';
|
||
l_rec.is_running := l_rec.status = 'IN_PROGRESS';
|
||
l_rec.is_pending := l_rec.status = 'PENDING';
|
||
RETURN l_rec;
|
||
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
RAISE_APPLICATION_ERROR(-20110,
|
||
'Projekt ' || p_project_id || ' nicht gefunden.');
|
||
END get_status;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE wait_for_completion(
|
||
p_project_id IN NUMBER,
|
||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||
p_interval_sec IN NUMBER DEFAULT 5
|
||
) IS
|
||
l_start TIMESTAMP := SYSTIMESTAMP;
|
||
l_status t_project_status;
|
||
BEGIN
|
||
LOOP
|
||
l_status := get_status(p_project_id);
|
||
|
||
EXIT WHEN l_status.is_completed;
|
||
|
||
IF EXTRACT(SECOND FROM (SYSTIMESTAMP - l_start))
|
||
+ EXTRACT(MINUTE FROM (SYSTIMESTAMP - l_start)) * 60
|
||
+ EXTRACT(HOUR FROM (SYSTIMESTAMP - l_start)) * 3600
|
||
> p_timeout_sec
|
||
THEN
|
||
RAISE_APPLICATION_ERROR(-20111,
|
||
'Timeout nach ' || p_timeout_sec || 's – Projekt ' ||
|
||
p_project_id || ' noch im Status ' || l_status.status ||
|
||
' (' || l_status.processing_progress || '%).');
|
||
END IF;
|
||
|
||
DBMS_SESSION.SLEEP(p_interval_sec);
|
||
END LOOP;
|
||
END wait_for_completion;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION convert_markdown(
|
||
p_markdown IN CLOB,
|
||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||
) RETURN BLOB IS
|
||
l_url VARCHAR2(500);
|
||
l_body CLOB;
|
||
l_result BLOB;
|
||
l_status NUMBER;
|
||
BEGIN
|
||
l_url := c_base_url || '/export/markdown';
|
||
|
||
-- JSON-Body mit CLOB-Inhalt: APEX_JSON unterstützt CLOBs beliebiger Größe
|
||
APEX_JSON.INITIALIZE_CLOB_OUTPUT;
|
||
APEX_JSON.OPEN_OBJECT;
|
||
APEX_JSON.WRITE('content', p_markdown);
|
||
APEX_JSON.WRITE('format', UPPER(p_format));
|
||
APEX_JSON.CLOSE_OBJECT;
|
||
l_body := APEX_JSON.GET_CLOB_OUTPUT;
|
||
APEX_JSON.FREE_OUTPUT;
|
||
|
||
set_headers();
|
||
|
||
l_result := APEX_WEB_SERVICE.MAKE_REST_REQUEST_B(
|
||
p_url => l_url,
|
||
p_http_method => 'POST',
|
||
p_body => l_body
|
||
);
|
||
|
||
l_status := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||
|
||
IF l_status NOT IN (200, 201) THEN
|
||
RAISE_APPLICATION_ERROR(-20200,
|
||
'Export-Service Fehler: HTTP ' || l_status ||
|
||
' (format=' || p_format || ')');
|
||
END IF;
|
||
|
||
RETURN l_result;
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
IF SQLCODE = -20200 THEN RAISE; END IF;
|
||
RAISE_APPLICATION_ERROR(-20201,
|
||
'Verbindungsfehler beim Export: ' || SQLERRM);
|
||
END convert_markdown;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION project_report(
|
||
p_project_id IN NUMBER,
|
||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||
) RETURN BLOB IS
|
||
l_markdown CLOB;
|
||
BEGIN
|
||
SELECT report_markdown
|
||
INTO l_markdown
|
||
FROM dc_projects
|
||
WHERE id = p_project_id;
|
||
|
||
IF l_markdown IS NULL OR DBMS_LOB.GETLENGTH(l_markdown) = 0 THEN
|
||
RAISE_APPLICATION_ERROR(-20202,
|
||
'Projekt ' || p_project_id || ' hat noch keinen Markdown-Bericht. '
|
||
|| 'Bitte zuerst POST /api/dc/projects/' || p_project_id || '/report aufrufen.');
|
||
END IF;
|
||
|
||
RETURN convert_markdown(l_markdown, p_format);
|
||
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
RAISE_APPLICATION_ERROR(-20110,
|
||
'Projekt ' || p_project_id || ' nicht gefunden.');
|
||
END project_report;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE regenerate_report(
|
||
p_project_id IN NUMBER
|
||
) IS
|
||
BEGIN
|
||
DC_UTILS_PKG.generate_report(p_project_id => p_project_id);
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
RAISE_APPLICATION_ERROR(-20110,
|
||
'Projekt ' || p_project_id || ' nicht gefunden.');
|
||
END regenerate_report;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Interne Hilfsfunktion: schreibt einen VARCHAR2-Text als UTF8-Bytes in ein BLOB.
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE append_text_to_blob(p_dest IN OUT BLOB, p_text IN VARCHAR2) IS
|
||
l_raw RAW(32767);
|
||
BEGIN
|
||
l_raw := UTL_RAW.CAST_TO_RAW(p_text);
|
||
DBMS_LOB.WRITEAPPEND(p_dest, UTL_RAW.LENGTH(l_raw), l_raw);
|
||
END append_text_to_blob;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION get_catalog_status(
|
||
p_catalog_id IN NUMBER
|
||
) RETURN VARCHAR2 IS
|
||
l_status VARCHAR2(20);
|
||
BEGIN
|
||
SELECT generation_status
|
||
INTO l_status
|
||
FROM dc_question_catalogs
|
||
WHERE id = p_catalog_id;
|
||
RETURN l_status;
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
RAISE_APPLICATION_ERROR(-20310,
|
||
'Katalog ' || p_catalog_id || ' nicht gefunden.');
|
||
END get_catalog_status;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
PROCEDURE wait_for_catalog(
|
||
p_catalog_id IN NUMBER,
|
||
p_timeout_sec IN NUMBER DEFAULT 1800,
|
||
p_interval_sec IN NUMBER DEFAULT 10
|
||
) IS
|
||
l_start TIMESTAMP := SYSTIMESTAMP;
|
||
l_status VARCHAR2(20);
|
||
l_elapsed NUMBER;
|
||
BEGIN
|
||
LOOP
|
||
l_status := get_catalog_status(p_catalog_id);
|
||
|
||
IF l_status = 'READY' THEN
|
||
RETURN;
|
||
END IF;
|
||
|
||
IF l_status = 'ERROR' THEN
|
||
DECLARE
|
||
l_error VARCHAR2(4000);
|
||
BEGIN
|
||
SELECT generation_error
|
||
INTO l_error
|
||
FROM dc_question_catalogs
|
||
WHERE id = p_catalog_id;
|
||
RAISE_APPLICATION_ERROR(-20311,
|
||
'Katalog ' || p_catalog_id || ' Generierung fehlgeschlagen: '
|
||
|| NVL(l_error, 'Unbekannter Fehler'));
|
||
END;
|
||
END IF;
|
||
|
||
-- Timeout prüfen
|
||
l_elapsed :=
|
||
EXTRACT(SECOND FROM (SYSTIMESTAMP - l_start))
|
||
+ EXTRACT(MINUTE FROM (SYSTIMESTAMP - l_start)) * 60
|
||
+ EXTRACT(HOUR FROM (SYSTIMESTAMP - l_start)) * 3600;
|
||
|
||
IF l_elapsed > p_timeout_sec THEN
|
||
RAISE_APPLICATION_ERROR(-20312,
|
||
'Timeout nach ' || p_timeout_sec || 's – Katalog ' ||
|
||
p_catalog_id || ' noch im Status ' || l_status || '.');
|
||
END IF;
|
||
|
||
DBMS_SESSION.SLEEP(p_interval_sec);
|
||
END LOOP;
|
||
END wait_for_catalog;
|
||
|
||
|
||
-- -------------------------------------------------------------------------
|
||
FUNCTION generate_catalog(
|
||
p_file IN BLOB,
|
||
p_mime_type IN VARCHAR2 DEFAULT 'application/pdf',
|
||
p_filename IN VARCHAR2 DEFAULT 'dokument.pdf',
|
||
p_catalog_name IN VARCHAR2,
|
||
p_document_type_id IN NUMBER,
|
||
p_description IN VARCHAR2 DEFAULT NULL
|
||
) RETURN NUMBER IS
|
||
|
||
c_crlf CONSTANT VARCHAR2(2) := CHR(13) || CHR(10);
|
||
l_boundary VARCHAR2(50) := 'dc-boundary-' || TO_CHAR(SYSDATE, 'YYYYMMDDHHMI')
|
||
|| DBMS_RANDOM.STRING('X', 8);
|
||
l_url VARCHAR2(500);
|
||
l_body BLOB;
|
||
l_response CLOB;
|
||
l_status NUMBER;
|
||
l_catalog_id NUMBER;
|
||
|
||
-- Hilfsfunktion: multipart-Textfeld anfügen
|
||
PROCEDURE add_field(p_name VARCHAR2, p_value VARCHAR2) IS
|
||
BEGIN
|
||
IF p_value IS NULL THEN RETURN; END IF;
|
||
append_text_to_blob(l_body,
|
||
'--' || l_boundary || c_crlf
|
||
|| 'Content-Disposition: form-data; name="' || p_name || '"' || c_crlf
|
||
|| c_crlf
|
||
|| p_value || c_crlf);
|
||
END add_field;
|
||
|
||
BEGIN
|
||
l_url := c_base_url || '/catalog/generate';
|
||
|
||
-- ---- multipart-Body aufbauen ----
|
||
DBMS_LOB.CREATETEMPORARY(l_body, TRUE);
|
||
|
||
-- Textfelder
|
||
add_field('catalog_name', p_catalog_name);
|
||
add_field('document_type_id', TO_CHAR(p_document_type_id));
|
||
add_field('description', p_description);
|
||
|
||
-- Datei-Part: Header
|
||
append_text_to_blob(l_body,
|
||
'--' || l_boundary || c_crlf
|
||
|| 'Content-Disposition: form-data; name="file"; filename="'
|
||
|| p_filename || '"' || c_crlf
|
||
|| 'Content-Type: ' || p_mime_type || c_crlf
|
||
|| c_crlf);
|
||
|
||
-- Datei-Part: BLOB-Inhalt anhängen
|
||
DBMS_LOB.APPEND(l_body, p_file);
|
||
|
||
-- Datei-Part: Abschluss-CRLF
|
||
append_text_to_blob(l_body, c_crlf);
|
||
|
||
-- Abschließende Boundary
|
||
append_text_to_blob(l_body, '--' || l_boundary || '--' || c_crlf);
|
||
|
||
-- ---- HTTP-Request senden ----
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).NAME := 'Content-Type';
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).VALUE :=
|
||
'multipart/form-data; boundary=' || l_boundary;
|
||
|
||
IF g_api_key IS NOT NULL AND LENGTH(TRIM(g_api_key)) > 0 THEN
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).NAME := 'X-API-KEY';
|
||
APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).VALUE := g_api_key;
|
||
END IF;
|
||
|
||
l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
|
||
p_url => l_url,
|
||
p_http_method => 'POST',
|
||
p_body_blob => l_body
|
||
);
|
||
DBMS_LOB.FREETEMPORARY(l_body);
|
||
|
||
l_status := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||
|
||
-- 202 Accepted = asynchrone Verarbeitung gestartet
|
||
IF l_status NOT IN (200, 201, 202) THEN
|
||
RAISE_APPLICATION_ERROR(-20300,
|
||
'Katalog-Generierung fehlgeschlagen: HTTP ' || l_status
|
||
|| ' – ' || SUBSTR(l_response, 1, 500));
|
||
END IF;
|
||
|
||
-- catalog_id aus JSON-Antwort lesen:
|
||
-- {"catalog_id": 42, "catalog_name": "...", "generation_status": "GENERATING"}
|
||
l_catalog_id := TO_NUMBER(JSON_VALUE(l_response, '$.catalog_id'));
|
||
|
||
IF l_catalog_id IS NULL THEN
|
||
RAISE_APPLICATION_ERROR(-20301,
|
||
'Antwort enthält keine catalog_id: ' || SUBSTR(l_response, 1, 500));
|
||
END IF;
|
||
|
||
RETURN l_catalog_id;
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
IF DBMS_LOB.ISTEMPORARY(l_body) = 1 THEN
|
||
DBMS_LOB.FREETEMPORARY(l_body);
|
||
END IF;
|
||
IF SQLCODE IN (-20300, -20301) THEN RAISE; END IF;
|
||
RAISE_APPLICATION_ERROR(-20302,
|
||
'Verbindungsfehler bei Katalog-Generierung: ' || SQLERRM);
|
||
END generate_catalog;
|
||
|
||
|
||
END DC_BACKEND_PKG;
|
||
/
|
||
|
||
|
||
-- =============================================================================
|
||
-- Schnelltest (optional, auskommentiert)
|
||
-- =============================================================================
|
||
/*
|
||
-- Health-Check
|
||
BEGIN
|
||
IF DC_BACKEND_PKG.health_check() THEN
|
||
DBMS_OUTPUT.PUT_LINE('dc-backend: UP');
|
||
ELSE
|
||
DBMS_OUTPUT.PUT_LINE('dc-backend: NICHT ERREICHBAR');
|
||
END IF;
|
||
END;
|
||
/
|
||
|
||
-- Prüfung starten (Projekt-ID anpassen)
|
||
DECLARE
|
||
l_status NUMBER;
|
||
l_response VARCHAR2(4000);
|
||
BEGIN
|
||
DC_BACKEND_PKG.start_check(
|
||
p_project_id => 1,
|
||
p_status_code => l_status,
|
||
p_response => l_response
|
||
);
|
||
DBMS_OUTPUT.PUT_LINE('Status: ' || l_status);
|
||
DBMS_OUTPUT.PUT_LINE('Response: ' || l_response);
|
||
END;
|
||
/
|
||
|
||
-- Bericht als PDF exportieren
|
||
DECLARE
|
||
l_blob BLOB;
|
||
BEGIN
|
||
DC_BACKEND_PKG.g_api_key := 'mein-api-key';
|
||
l_blob := DC_BACKEND_PKG.project_report(p_project_id => 1, p_format => 'PDF');
|
||
DBMS_OUTPUT.PUT_LINE('PDF-Größe: ' || DBMS_LOB.GETLENGTH(l_blob) || ' Bytes');
|
||
END;
|
||
/
|
||
|
||
-- Bericht als DOCX exportieren
|
||
DECLARE
|
||
l_blob BLOB;
|
||
BEGIN
|
||
DC_BACKEND_PKG.g_api_key := 'mein-api-key';
|
||
l_blob := DC_BACKEND_PKG.project_report(p_project_id => 1, p_format => 'DOCX');
|
||
DBMS_OUTPUT.PUT_LINE('DOCX-Größe: ' || DBMS_LOB.GETLENGTH(l_blob) || ' Bytes');
|
||
END;
|
||
/
|
||
*/
|