mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-07-31 20:25:33 +00:00
Merge pull request #5 from Sudo-JHare/Hapi-Integration
Full blown rework , ui , hapi , gofsh, sushi
This commit is contained in:
commit
454203a583
209
Build and Run for first time.bat
Normal file
209
Build and Run for first time.bat
Normal file
@ -0,0 +1,209 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM --- Configuration ---
|
||||
set REPO_URL=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
|
||||
set CLONE_DIR=hapi-fhir-jpaserver
|
||||
set SOURCE_CONFIG_DIR=hapi-fhir-setup
|
||||
set CONFIG_FILE=application.yaml
|
||||
|
||||
REM --- Define Paths ---
|
||||
set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
|
||||
set DEST_CONFIG_PATH=%CLONE_DIR%\target\classes\%CONFIG_FILE%
|
||||
|
||||
REM === CORRECTED: Prompt for Version ===
|
||||
:GetModeChoice
|
||||
SET "APP_MODE=" REM Clear the variable first
|
||||
echo Select Installation Mode:
|
||||
echo 1. Standalone (Includes local HAPI FHIR Server - Requires Git & Maven)
|
||||
echo 2. Lite (Excludes local HAPI FHIR Server - No Git/Maven needed)
|
||||
CHOICE /C 12 /N /M "Enter your choice (1 or 2):"
|
||||
|
||||
IF ERRORLEVEL 2 (
|
||||
SET APP_MODE=lite
|
||||
goto :ModeSet
|
||||
)
|
||||
IF ERRORLEVEL 1 (
|
||||
SET APP_MODE=standalone
|
||||
goto :ModeSet
|
||||
)
|
||||
REM If somehow neither was chosen (e.g., Ctrl+C), loop back
|
||||
echo Invalid input. Please try again.
|
||||
goto :GetModeChoice
|
||||
|
||||
:ModeSet
|
||||
IF "%APP_MODE%"=="" (
|
||||
echo Invalid choice detected after checks. Exiting.
|
||||
goto :eof
|
||||
)
|
||||
echo Selected Mode: %APP_MODE%
|
||||
echo.
|
||||
REM === END CORRECTION ===
|
||||
|
||||
|
||||
REM === Conditionally Execute HAPI Setup ===
|
||||
IF "%APP_MODE%"=="standalone" (
|
||||
echo Running Standalone setup including HAPI FHIR...
|
||||
echo.
|
||||
|
||||
REM --- Step 0: Clean up previous clone (optional) ---
|
||||
echo Checking for existing directory: %CLONE_DIR%
|
||||
if exist "%CLONE_DIR%" (
|
||||
echo Found existing directory, removing it...
|
||||
rmdir /s /q "%CLONE_DIR%"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Failed to remove existing directory: %CLONE_DIR%
|
||||
goto :error
|
||||
)
|
||||
echo Existing directory removed.
|
||||
) else (
|
||||
echo Directory does not exist, proceeding with clone.
|
||||
)
|
||||
echo.
|
||||
|
||||
REM --- Step 1: Clone the HAPI FHIR server repository ---
|
||||
echo Cloning repository: %REPO_URL% into %CLONE_DIR%...
|
||||
git clone "%REPO_URL%" "%CLONE_DIR%"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Failed to clone repository. Check Git installation and network connection.
|
||||
goto :error
|
||||
)
|
||||
echo Repository cloned successfully.
|
||||
echo.
|
||||
|
||||
REM --- Step 2: Navigate into the cloned directory ---
|
||||
echo Changing directory to %CLONE_DIR%...
|
||||
cd "%CLONE_DIR%"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Failed to change directory to %CLONE_DIR%.
|
||||
goto :error
|
||||
)
|
||||
echo Current directory: %CD%
|
||||
echo.
|
||||
|
||||
REM --- Step 3: Build the HAPI server using Maven ---
|
||||
echo ===> "Starting Maven build (Step 3)...""
|
||||
cmd /c "mvn clean package -DskipTests=true -Pboot"
|
||||
echo ===> Maven command finished. Checking error level...
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Maven build failed or cmd /c failed
|
||||
cd ..
|
||||
goto :error
|
||||
)
|
||||
echo Maven build completed successfully. ErrorLevel: %errorlevel%
|
||||
echo.
|
||||
|
||||
REM --- Step 4: Copy the configuration file ---
|
||||
echo ===> "Starting file copy (Step 4)..."
|
||||
echo Copying configuration file...
|
||||
echo Source: %SOURCE_CONFIG_PATH%
|
||||
echo Destination: target\classes\%CONFIG_FILE%
|
||||
xcopy "%SOURCE_CONFIG_PATH%" "target\classes\" /Y /I
|
||||
echo ===> xcopy command finished. Checking error level...
|
||||
if errorlevel 1 (
|
||||
echo WARNING: Failed to copy configuration file. Check if the source file exists.
|
||||
echo The script will continue, but the server might use default configuration.
|
||||
) else (
|
||||
echo Configuration file copied successfully. ErrorLevel: %errorlevel%
|
||||
)
|
||||
echo.
|
||||
|
||||
REM --- Step 5: Navigate back to the parent directory ---
|
||||
echo ===> "Changing directory back (Step 5)..."
|
||||
cd ..
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Failed to change back to the parent directory. ErrorLevel: %errorlevel%
|
||||
goto :error
|
||||
)
|
||||
echo Current directory: %CD%
|
||||
echo.
|
||||
|
||||
) ELSE (
|
||||
echo Running Lite setup, skipping HAPI FHIR build...
|
||||
REM Ensure the hapi-fhir-jpaserver directory doesn't exist or is empty if Lite mode is chosen after a standalone attempt
|
||||
if exist "%CLONE_DIR%" (
|
||||
echo Found existing HAPI directory in Lite mode. Removing it to avoid build issues...
|
||||
rmdir /s /q "%CLONE_DIR%"
|
||||
)
|
||||
REM Create empty target directories expected by Dockerfile COPY, even if not used
|
||||
mkdir "%CLONE_DIR%\target\classes" 2> nul
|
||||
mkdir "%CLONE_DIR%\custom" 2> nul
|
||||
REM Create a placeholder empty WAR file to satisfy Dockerfile COPY
|
||||
echo. > "%CLONE_DIR%\target\ROOT.war"
|
||||
echo. > "%CLONE_DIR%\target\classes\application.yaml"
|
||||
echo Placeholder files created for Lite mode build.
|
||||
echo.
|
||||
)
|
||||
|
||||
REM === Modify docker-compose.yml to set APP_MODE ===
|
||||
echo Updating docker-compose.yml with APP_MODE=%APP_MODE%...
|
||||
(
|
||||
echo version: '3.8'
|
||||
echo services:
|
||||
echo fhirflare:
|
||||
echo build:
|
||||
echo context: .
|
||||
echo dockerfile: Dockerfile
|
||||
echo ports:
|
||||
echo - "5000:5000"
|
||||
echo - "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
||||
echo volumes:
|
||||
echo - ./instance:/app/instance
|
||||
echo - ./static/uploads:/app/static/uploads
|
||||
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
||||
echo - ./logs:/app/logs
|
||||
echo environment:
|
||||
echo - FLASK_APP=app.py
|
||||
echo - FLASK_ENV=development
|
||||
echo - NODE_PATH=/usr/lib/node_modules
|
||||
echo - APP_MODE=%APP_MODE%
|
||||
echo command: supervisord -c /etc/supervisord.conf
|
||||
) > docker-compose.yml.tmp
|
||||
|
||||
REM Check if docker-compose.yml.tmp was created successfully
|
||||
if not exist docker-compose.yml.tmp (
|
||||
echo ERROR: Failed to create temporary docker-compose file.
|
||||
goto :error
|
||||
)
|
||||
|
||||
REM Replace the original docker-compose.yml
|
||||
del docker-compose.yml /Q > nul 2>&1
|
||||
ren docker-compose.yml.tmp docker-compose.yml
|
||||
echo docker-compose.yml updated successfully.
|
||||
echo.
|
||||
|
||||
REM --- Step 6: Build Docker images ---
|
||||
echo ===> Starting Docker build (Step 6)...
|
||||
docker-compose build --no-cache
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Docker Compose build failed. Check Docker installation and docker-compose.yml file. ErrorLevel: %errorlevel%
|
||||
goto :error
|
||||
)
|
||||
echo Docker images built successfully. ErrorLevel: %errorlevel%
|
||||
echo.
|
||||
|
||||
REM --- Step 7: Start Docker containers ---
|
||||
echo ===> Starting Docker containers (Step 7)...
|
||||
docker-compose up -d
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Docker Compose up failed. Check Docker installation and container configurations. ErrorLevel: %errorlevel%
|
||||
goto :error
|
||||
)
|
||||
echo Docker containers started successfully. ErrorLevel: %errorlevel%
|
||||
echo.
|
||||
|
||||
echo ====================================
|
||||
echo Script finished successfully! (Mode: %APP_MODE%)
|
||||
echo ====================================
|
||||
goto :eof
|
||||
|
||||
:error
|
||||
echo ------------------------------------
|
||||
echo An error occurred. Script aborted.
|
||||
echo ------------------------------------
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:eof
|
||||
echo Script execution finished.
|
||||
pause
|
26
DockerCommands.MD
Normal file
26
DockerCommands.MD
Normal file
@ -0,0 +1,26 @@
|
||||
Docker Commands.MD
|
||||
|
||||
|
||||
<HAPI-server.>
|
||||
to pull and clone:
|
||||
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver
|
||||
|
||||
to build:
|
||||
mvn clean package -DskipTests=true -Pboot
|
||||
|
||||
to run:
|
||||
java -jar target/ROOT.war
|
||||
|
||||
|
||||
<rest-of-the-app:>
|
||||
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
|
||||
|
||||
|
||||
<useful-stuff:>
|
||||
|
||||
cp <CONTAINERID>:/app/PATH/Filename.ext . - . copies to the root folder you ran it from
|
||||
|
||||
docker exec -it <CONTAINERID> bash - to get a bash - session in the container -
|
54
Dockerfile
54
Dockerfile
@ -1,13 +1,31 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
# Base image with Python and Java
|
||||
FROM tomcat:10.1-jdk17
|
||||
|
||||
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip python3-venv curl coreutils \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Install specific versions of GoFSH and SUSHI
|
||||
# REMOVED pip install fhirpath from this line
|
||||
RUN npm install -g gofsh fsh-sushi
|
||||
|
||||
# Set up Python environment
|
||||
WORKDIR /app
|
||||
RUN python3 -m venv /app/venv
|
||||
ENV PATH="/app/venv/bin:$PATH"
|
||||
|
||||
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
||||
RUN pip uninstall -y fhirpath || true
|
||||
# ADDED: Install the new fhirpathpy library
|
||||
RUN pip install --no-cache-dir fhirpathpy
|
||||
|
||||
# Copy Flask files
|
||||
COPY requirements.txt .
|
||||
# Install requirements (including Pydantic - check version compatibility if needed)
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app.py .
|
||||
COPY services.py .
|
||||
COPY forms.py .
|
||||
@ -15,10 +33,24 @@ COPY templates/ templates/
|
||||
COPY static/ static/
|
||||
COPY tests/ tests/
|
||||
|
||||
# Ensure /tmp is writable as a fallback
|
||||
RUN mkdir -p /tmp && chmod 777 /tmp
|
||||
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
|
||||
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
|
||||
|
||||
EXPOSE 5000
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_ENV=development
|
||||
CMD ["flask", "run", "--host=0.0.0.0"]
|
||||
# Copy pre-built HAPI WAR and configuration
|
||||
COPY hapi-fhir-jpaserver/target/ROOT.war /usr/local/tomcat/webapps/
|
||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/conf/
|
||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application.yaml
|
||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
||||
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
||||
|
||||
# Install supervisord
|
||||
RUN pip install supervisor
|
||||
|
||||
# Configure supervisord
|
||||
COPY supervisord.conf /etc/supervisord.conf
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 5000 8080
|
||||
|
||||
# Start supervisord
|
||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
738
README.md
738
README.md
@ -2,307 +2,611 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The FHIRFLARE IG Toolkit is a Flask-based web application designed to simplify the management, processing, validation, and deployment of FHIR Implementation Guides (IGs). It allows users to import IG packages, process them to extract resource types and profiles, validate FHIR resources or bundles against IGs, and push IGs to a FHIR server. The application features a user-friendly interface with a live console for real-time feedback during operations like pushing IGs or validating resources.
|
||||
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs). It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, and converting FHIR resources to FHIR Shorthand (FSH) using GoFSH with advanced features. The toolkit includes a live console for real-time feedback and a waiting spinner for FSH conversion, making it an essential tool for FHIR developers and implementers.
|
||||
|
||||
The application can run in two modes:
|
||||
|
||||
* **Standalone:** Includes a Dockerized Flask frontend, SQLite database, and an embedded HAPI FHIR server for local validation and interaction.
|
||||
* **Lite:** Includes only the Dockerized Flask frontend and SQLite database, excluding the local HAPI FHIR server. Requires connection to external FHIR servers for certain features.
|
||||
|
||||
## Installation Modes (Lite vs. Standalone)
|
||||
|
||||
This toolkit offers two primary installation modes to suit different needs:
|
||||
|
||||
* **Standalone Version:**
|
||||
* Includes the full FHIRFLARE Toolkit application **and** an embedded HAPI FHIR server running locally within the Docker environment.
|
||||
* Allows for local FHIR resource validation using HAPI FHIR's capabilities.
|
||||
* Enables the "Use Local HAPI" option in the FHIR API Explorer and FHIR UI Operations pages, proxying requests to the internal HAPI server (`http://localhost:8080/fhir`).
|
||||
* Requires Git and Maven during the initial build process (via the `.bat` script or manual steps) to prepare the HAPI FHIR server.
|
||||
* Ideal for users who want a self-contained environment for development and testing or who don't have readily available external FHIR servers.
|
||||
|
||||
* **Lite Version:**
|
||||
* Includes the FHIRFLARE Toolkit application **without** the embedded HAPI FHIR server.
|
||||
* Requires users to provide URLs for external FHIR servers when using features like the FHIR API Explorer and FHIR UI Operations pages. The "Use Local HAPI" option will be disabled in the UI.
|
||||
* Resource validation relies solely on local checks against downloaded StructureDefinitions, which may be less comprehensive than HAPI FHIR's validation (e.g., for terminology bindings or complex invariants).
|
||||
* **Does not require Git or Maven** for setup if using the `.bat` script or running the pre-built Docker image.
|
||||
* Ideal for users who primarily want to use the IG management, processing, and FSH conversion features, or who will always connect to existing external FHIR servers.
|
||||
|
||||
## Features
|
||||
|
||||
- **Import IGs**: Download FHIR IG packages and their dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `1.1.2-ballot`, `current`).
|
||||
- **Manage IGs**: View, process, unload, or delete downloaded IGs, with duplicate detection and resolution.
|
||||
- **Process IGs**: Extract resource types, profiles, must-support elements, examples, and profile relationships (`compliesWithProfile` and `imposeProfile`) from IGs.
|
||||
- **Validate FHIR Resources/Bundles**: Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature, work in progress).
|
||||
- **Push IGs**: Upload IG resources to a FHIR server with real-time console output, including validation against imposed profiles.
|
||||
- **Profile Relationships**: Support for `structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile` extensions, with validation and UI display.
|
||||
- **API Support**: RESTful API endpoints for importing, pushing, and validating IGs, including profile relationship metadata.
|
||||
- **Live Console**: Displays real-time logs during push and validation operations.
|
||||
- **Configurable Behavior**: Options to enable/disable imposed profile validation and UI display of profile relationships.
|
||||
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `1.1.2-ballot`, `current`).
|
||||
* **Manage IGs:** View, process, unload, or delete downloaded IGs, with duplicate detection and resolution.
|
||||
* **Process IGs:** Extract resource types, profiles, must-support elements, examples, and profile relationships (`structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile`).
|
||||
* **Validate FHIR Resources/Bundles:** Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature, work in progress). *Note: Lite version uses local SD checks only.*
|
||||
* **Push IGs:** Upload IG resources to a target FHIR server with real-time console output and optional validation against imposed profiles.
|
||||
* **Profile Relationships:** Display and validate `compliesWithProfile` and `imposeProfile` extensions in the UI.
|
||||
* **FSH Converter:** Convert FHIR JSON/XML resources to FHIR Shorthand (FSH) using GoFSH, with advanced options (Package context, Output styles, Log levels, FHIR versions, Fishing Trip, Dependencies, Indentation, Meta Profile handling, Alias File, No Alias). Includes a waiting spinner.
|
||||
* **FHIR Interaction UIs:** Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" and "FHIR UI Operations" pages. *Note: Lite version requires custom server URLs.*
|
||||
* **API Support:** RESTful API endpoints for importing, pushing, and retrieving IG metadata.
|
||||
* **Live Console:** Real-time logs for push, validation, and FSH conversion operations.
|
||||
* **Configurable Behavior:** Enable/disable imposed profile validation and UI display of profile relationships.
|
||||
* **Theming:** Supports light and dark modes.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
The FHIRFLARE IG Toolkit is built using the following technologies:
|
||||
|
||||
- **Python 3.9+**: Core programming language for the backend.
|
||||
- **Flask 2.0+**: Lightweight web framework for building the application.
|
||||
- **Flask-SQLAlchemy**: ORM for managing the SQLite database.
|
||||
- **Flask-WTF**: Handles form creation, validation, and CSRF protection.
|
||||
- **Jinja2**: Templating engine for rendering HTML pages.
|
||||
- **Bootstrap 5**: Frontend framework for responsive UI design.
|
||||
- **JavaScript (ES6)**: Client-side scripting for interactive features like the live console and JSON validation preview.
|
||||
- **SQLite**: Lightweight database for storing processed IG metadata.
|
||||
- **Docker**: Containerization for consistent deployment.
|
||||
- **Requests**: Python library for making HTTP requests to FHIR servers.
|
||||
- **Tarfile**: Python library for handling `.tgz` package files.
|
||||
- **Logging**: Python's built-in logging for debugging and monitoring.
|
||||
* Python 3.12+, Flask 2.3.3, Flask-SQLAlchemy 3.0.5, Flask-WTF 1.2.1
|
||||
* Jinja2, Bootstrap 5.3.3, JavaScript (ES6), Lottie-Web 5.12.2
|
||||
* SQLite
|
||||
* Docker, Docker Compose, Supervisor
|
||||
* Node.js 18+ (for GoFSH/SUSHI), GoFSH, SUSHI
|
||||
* HAPI FHIR (Standalone version only)
|
||||
* Requests 2.31.0, Tarfile, Logging
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.9+**: Ensure Python is installed on your system.
|
||||
- **Docker**: Required for containerized deployment.
|
||||
- **pip**: Python package manager for installing dependencies.
|
||||
* **Docker:** Required for containerized deployment (both versions).
|
||||
* **Git & Maven:** Required **only** for building the **Standalone** version from source using the `.bat` script or manual steps. Not required for the Lite version build or for running pre-built Docker Hub images.
|
||||
* **Windows:** Required if using the `.bat` scripts.
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
1. **Clone the Repository**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd fhirflare-ig-toolkit
|
||||
```
|
||||
### Running Pre-built Images (General Users)
|
||||
|
||||
2. **Install Dependencies**:
|
||||
Create a virtual environment and install the required packages:
|
||||
```bash
|
||||
This is the easiest way to get started without needing Git or Maven. Choose the version you need:
|
||||
|
||||
**Lite Version (No local HAPI FHIR):**
|
||||
|
||||
# Pull the latest Lite image
|
||||
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
|
||||
|
||||
# Run the Lite version (maps port 5000 for the UI)
|
||||
# You'll need to create local directories for persistent data first:
|
||||
# mkdir instance logs static static/uploads instance/hapi-h2-data
|
||||
docker run -d \
|
||||
-p 5000:5000 \
|
||||
-v ./instance:/app/instance \
|
||||
-v ./static/uploads:/app/static/uploads \
|
||||
-v ./instance/hapi-h2-data:/app/h2-data \
|
||||
-v ./logs:/app/logs \
|
||||
--name fhirflare-lite \
|
||||
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
|
||||
|
||||
|
||||
|
||||
# Pull the latest Standalone image
|
||||
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
||||
|
||||
# Run the Standalone version (maps ports 5000 and 8080)
|
||||
# You'll need to create local directories for persistent data first:
|
||||
# mkdir instance logs static static/uploads instance/hapi-h2-data
|
||||
docker run -d \
|
||||
-p 5000:5000 \
|
||||
-p 8080:8080 \
|
||||
-v ./instance:/app/instance \
|
||||
-v ./static/uploads:/app/static/uploads \
|
||||
-v ./instance/hapi-h2-data:/app/h2-data \
|
||||
-v ./logs:/app/logs \
|
||||
--name fhirflare-standalone \
|
||||
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
||||
|
||||
|
||||
Run Build and Run for first time.bat:
|
||||
cd "<project folder>"
|
||||
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver
|
||||
copy .\hapi-fhir-Setup\target\classes\application.yaml .\hapi-fhir-jpaserver\target\classes\application.yaml
|
||||
mvn clean package -DskipTests=true -Pboot
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
|
||||
This clones the HAPI FHIR server, copies configuration, builds the project, and starts the containers.
|
||||
|
||||
|
||||
Subsequent Runs:
|
||||
|
||||
Run Run.bat:
|
||||
cd "<project folder>"
|
||||
docker-compose up -d
|
||||
|
||||
This starts the Flask app (port 5000) and HAPI FHIR server (port 8080).
|
||||
|
||||
|
||||
Access the Application:
|
||||
|
||||
Flask UI: http://localhost:5000
|
||||
HAPI FHIR server: http://localhost:8080
|
||||
|
||||
Manual Setup (Linux/MacOS/Windows)
|
||||
Preparation:
|
||||
cd <project folder>
|
||||
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver
|
||||
cp ./hapi-fhir-Setup/target/classes/application.yaml ./hapi-fhir-jpaserver/target/classes/application.yaml
|
||||
|
||||
Build:
|
||||
mvn clean package -DskipTests=true -Pboot
|
||||
docker-compose build --no-cache
|
||||
|
||||
Run:
|
||||
docker-compose up -d
|
||||
|
||||
Access the Application:
|
||||
|
||||
Flask UI: http://localhost:5000
|
||||
HAPI FHIR server: http://localhost:8080
|
||||
|
||||
Local Development (Without Docker)
|
||||
Clone the Repository:
|
||||
git clone https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
|
||||
cd FHIRFLARE-IG-Toolkit
|
||||
|
||||
Install Dependencies:
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Set Environment Variables**:
|
||||
Set the `FLASK_SECRET_KEY` and `API_KEY` for security and CSRF protection:
|
||||
```bash
|
||||
Install Node.js, GoFSH, and SUSHI (for FSH Converter):
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash -
|
||||
sudo apt-get install -y nodejs
|
||||
npm install -g gofsh fsh-sushi
|
||||
|
||||
Set Environment Variables:
|
||||
export FLASK_SECRET_KEY='your-secure-secret-key'
|
||||
export API_KEY='your-api-key'
|
||||
```
|
||||
|
||||
4. **Initialize the Database**:
|
||||
Ensure the `instance` directory is writable for SQLite:
|
||||
```bash
|
||||
mkdir -p instance
|
||||
chmod -R 777 instance
|
||||
```
|
||||
Initialize Directories:
|
||||
mkdir -p instance static/uploads logs
|
||||
chmod -R 777 instance static/uploads logs
|
||||
|
||||
5. **Run the Application Locally**:
|
||||
Start the Flask development server:
|
||||
```bash
|
||||
Run the Application:
|
||||
export FLASK_APP=app.py
|
||||
flask run
|
||||
```
|
||||
The application will be available at `http://localhost:5000`.
|
||||
|
||||
6. **Run with Docker**:
|
||||
Build and run the application using Docker, mounting the `instance` directory for persistence:
|
||||
```bash
|
||||
docker build -t flare-fhir-ig-toolkit .
|
||||
docker run -p 5000:5000 -e FLASK_SECRET_KEY='your-secure-secret-key' -e API_KEY='your-api-key' -v $(pwd)/instance:/app/instance flare-fhir-ig-toolkit
|
||||
```
|
||||
Access the application at `http://localhost:5000`.
|
||||
Access at http://localhost:5000.
|
||||
Usage
|
||||
Import an IG
|
||||
|
||||
## Usage
|
||||
Navigate to Import IG (/import-ig).
|
||||
Enter a package name (e.g., hl7.fhir.au.core) and version (e.g., 1.1.0-preview).
|
||||
Choose a dependency mode:
|
||||
Recursive: Import all dependencies.
|
||||
Patch Canonical: Import only canonical FHIR packages.
|
||||
Tree Shaking: Import only used dependencies.
|
||||
|
||||
1. **Import an IG**:
|
||||
- Navigate to the "Import IG" tab.
|
||||
- Enter a package name (e.g., `hl7.fhir.au.core`) and version (e.g., `1.1.0-preview`, `1.1.2-ballot`, or `current`).
|
||||
- Choose a dependency mode (e.g., Recursive, Tree Shaking).
|
||||
- Click "Import" to download the package and its dependencies.
|
||||
|
||||
2. **Manage IGs**:
|
||||
- Go to the "Manage FHIR Packages" tab to view downloaded and processed IGs.
|
||||
- Process IGs to extract metadata, unload processed IGs from the database, or delete packages from the filesystem.
|
||||
- Duplicates are highlighted for resolution (e.g., multiple versions of the same package).
|
||||
Click Import to download the package and dependencies.
|
||||
|
||||
3. **View Processed IGs**:
|
||||
- After processing an IG, view its details, including resource types, profiles, must-support elements, examples, and profile relationships (`compliesWithProfile` and `imposeProfile`).
|
||||
- Profile relationships are displayed if enabled via `DISPLAY_PROFILE_RELATIONSHIPS`.
|
||||
Manage IGs
|
||||
|
||||
4. **Validate FHIR Resources/Bundles**:
|
||||
- Navigate to the "Validate FHIR Sample" tab.
|
||||
- Select or enter a package (e.g., `hl7.fhir.au.core#1.1.0-preview`).
|
||||
- Choose validation mode (Single Resource or Bundle).
|
||||
- Paste FHIR JSON (e.g., a Patient or AllergyIntolerance resource).
|
||||
- Submit to validate, viewing errors and warnings in the report.
|
||||
- Note: Validation is in alpha; report issues to GitHub (remove PHI).
|
||||
Go to Manage FHIR Packages (/view-igs) to view downloaded and processed IGs.
|
||||
Actions:
|
||||
Process: Extract metadata (resource types, profiles, must-support elements, examples).
|
||||
Unload: Remove processed IG data from the database.
|
||||
Delete: Remove package files from the filesystem.
|
||||
|
||||
5. **Push IGs to a FHIR Server**:
|
||||
- Navigate to the "Push IGs" tab.
|
||||
- Select a package, enter a FHIR server URL (e.g., `http://hapi.fhir.org/baseR4`), and choose whether to include dependencies.
|
||||
- Click "Push to FHIR Server" to upload resources, with validation against imposed profiles (if enabled via `VALIDATE_IMPOSED_PROFILES`) and progress shown in the live console.
|
||||
|
||||
6. **API Usage**:
|
||||
- **Import IG**: `POST /api/import-ig`
|
||||
```bash
|
||||
Duplicates are highlighted for resolution.
|
||||
|
||||
View Processed IGs
|
||||
|
||||
After processing, view IG details (/view-ig/<id>), including:
|
||||
Resource types and profiles.
|
||||
Must-support elements and examples.
|
||||
Profile relationships (compliesWithProfile, imposeProfile) if enabled (DISPLAY_PROFILE_RELATIONSHIPS).
|
||||
|
||||
|
||||
|
||||
Validate FHIR Resources/Bundles
|
||||
|
||||
Navigate to Validate FHIR Sample (/validate-sample).
|
||||
Select a package (e.g., hl7.fhir.au.core#1.1.0-preview).
|
||||
Choose Single Resource or Bundle mode.
|
||||
Paste or upload FHIR JSON/XML (e.g., a Patient resource).
|
||||
Submit to view validation errors/warnings.
|
||||
Note: Alpha feature; report issues to GitHub (remove PHI).
|
||||
|
||||
Push IGs to a FHIR Server
|
||||
|
||||
Go to Push IGs (/push-igs).
|
||||
Select a package, enter a FHIR server URL (e.g., http://localhost:8080/fhir), and choose whether to include dependencies.
|
||||
Click Push to FHIR Server to upload resources, with validation against imposed profiles (if enabled via VALIDATE_IMPOSED_PROFILES).
|
||||
Monitor progress in the live console.
|
||||
|
||||
Convert FHIR to FSH
|
||||
|
||||
Navigate to FSH Converter (/fsh-converter).
|
||||
Optionally select a package for context (e.g., hl7.fhir.au.core#1.1.0-preview).
|
||||
Choose input mode:
|
||||
Upload File: Upload a FHIR JSON/XML file.
|
||||
Paste Text: Paste FHIR JSON/XML content.
|
||||
|
||||
|
||||
Configure options:
|
||||
Output Style: file-per-definition, group-by-fsh-type, group-by-profile, single-file.
|
||||
Log Level: error, warn, info, debug.
|
||||
FHIR Version: R4, R4B, R5, or auto-detect.
|
||||
Fishing Trip: Enable round-trip validation with SUSHI, generating a comparison report.
|
||||
Dependencies: Specify additional packages (e.g., hl7.fhir.us.core@6.1.0, one per line).
|
||||
Indent Rules: Enable context path indentation for readable FSH.
|
||||
Meta Profile: Choose only-one, first, or none for meta.profile handling.
|
||||
Alias File: Upload an FSH file with aliases (e.g., $MyAlias = http://example.org).
|
||||
No Alias: Disable automatic alias generation.
|
||||
|
||||
|
||||
Click Convert to FSH to generate and display FSH output, with a waiting spinner (light/dark theme) during processing.
|
||||
If Fishing Trip is enabled, view the comparison report via the "Click here for SUSHI Validation" badge button.
|
||||
Download the result as a .fsh file.
|
||||
|
||||
Example Input:
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
"id": "banks-mia-leanne",
|
||||
"meta": {
|
||||
"profile": ["http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient"]
|
||||
},
|
||||
"name": [
|
||||
{
|
||||
"family": "Banks",
|
||||
"given": ["Mia", "Leanne"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Example Output:
|
||||
Profile: AUCorePatient
|
||||
Parent: Patient
|
||||
* name 1..*
|
||||
* name.family 1..1
|
||||
* name.given 1..*
|
||||
|
||||
Explore FHIR Operations
|
||||
|
||||
Navigate to FHIR UI Operations (/fhir-ui-operations).
|
||||
Toggle between local HAPI (/fhir) or a custom FHIR server.
|
||||
Click Fetch Metadata to load the server’s CapabilityStatement.
|
||||
Select a resource type (e.g., Patient, Observation) or System to view operations:
|
||||
System operations: GET /metadata, POST /, GET /_history, GET/POST /$diff, POST /$reindex, POST /$expunge, etc.
|
||||
Resource operations: GET Patient/:id, POST Observation/_search, etc.
|
||||
|
||||
|
||||
Use Try it out to input parameters or request bodies, then Execute to view results in JSON, XML, or narrative formats.
|
||||
|
||||
API Usage
|
||||
Import IG
|
||||
curl -X POST http://localhost:5000/api/import-ig \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "api_key": "your-api-key"}'
|
||||
```
|
||||
Response includes `complies_with_profiles`, `imposed_profiles`, and duplicates.
|
||||
- **Push IG**: `POST /api/push-ig`
|
||||
```bash
|
||||
|
||||
Returns complies_with_profiles, imposed_profiles, and duplicate info.
|
||||
Push IG
|
||||
curl -X POST http://localhost:5000/api/push-ig \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/x-ndjson" \
|
||||
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "fhir_server_url": "http://hapi.fhir.org/baseR4", "include_dependencies": true, "api_key": "your-api-key"}'
|
||||
```
|
||||
Resources are validated against imposed profiles before pushing.
|
||||
- **Validate Resource/Bundle**: Not yet exposed via API; use the UI at `/validate-sample`.
|
||||
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "fhir_server_url": "http://localhost:8080/fhir", "include_dependencies": true, "api_key": "your-api-key"}'
|
||||
|
||||
## Configuration Options
|
||||
Validates resources against imposed profiles (if enabled).
|
||||
Validate Resource/Bundle
|
||||
Not yet exposed via API; use the UI at /validate-sample.
|
||||
Configuration Options
|
||||
|
||||
- **`VALIDATE_IMPOSED_PROFILES`**: Set to `True` (default) to validate resources against imposed profiles during push operations. Set to `False` to skip:
|
||||
```python
|
||||
VALIDATE_IMPOSED_PROFILES:
|
||||
|
||||
Default: True
|
||||
|
||||
Validates resources against imposed profiles during push.
|
||||
|
||||
Set to False to skip:
|
||||
app.config['VALIDATE_IMPOSED_PROFILES'] = False
|
||||
```
|
||||
- **`DISPLAY_PROFILE_RELATIONSHIPS`**: Set to `True` (default) to show `compliesWithProfile` and `imposeProfile` in the UI. Set to `False` to hide:
|
||||
```python
|
||||
|
||||
|
||||
|
||||
|
||||
DISPLAY_PROFILE_RELATIONSHIPS:
|
||||
|
||||
Default: True
|
||||
|
||||
Shows compliesWithProfile and imposeProfile in the UI.
|
||||
|
||||
Set to False to hide:
|
||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = False
|
||||
```
|
||||
- **`FHIR_PACKAGES_DIR`**: Directory for storing `.tgz` packages and metadata (default: `/app/instance/fhir_packages`).
|
||||
- **`SECRET_KEY`**: Required for CSRF protection and session security:
|
||||
```python
|
||||
|
||||
|
||||
|
||||
|
||||
FHIR_PACKAGES_DIR:
|
||||
|
||||
Default: /app/instance/fhir_packages
|
||||
Stores .tgz packages and metadata.
|
||||
|
||||
|
||||
UPLOAD_FOLDER:
|
||||
|
||||
Default: /app/static/uploads
|
||||
Stores GoFSH output files and FSH comparison reports.
|
||||
|
||||
|
||||
SECRET_KEY:
|
||||
|
||||
Required for CSRF protection and sessions:
|
||||
app.config['SECRET_KEY'] = 'your-secure-secret-key'
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The project includes a test suite to ensure reliability, covering UI, API, database, file operations, and security features.
|
||||
|
||||
### Test Prerequisites
|
||||
|
||||
- **pytest**: For running tests.
|
||||
- **unittest-mock**: For mocking dependencies.
|
||||
API_KEY:
|
||||
|
||||
Install test dependencies:
|
||||
```bash
|
||||
Required for API authentication:
|
||||
app.config['API_KEY'] = 'your-api-key'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Testing
|
||||
The project includes a test suite covering UI, API, database, file operations, and security.
|
||||
Test Prerequisites
|
||||
|
||||
pytest: For running tests.
|
||||
pytest-mock: For mocking dependencies.
|
||||
|
||||
Install:
|
||||
pip install pytest pytest-mock
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
1. **Navigate to Project Root**:
|
||||
```bash
|
||||
cd /path/to/fhirflare-ig-toolkit
|
||||
```
|
||||
|
||||
2. **Run Tests**:
|
||||
```bash
|
||||
Running Tests
|
||||
cd <project folder>
|
||||
pytest tests/test_app.py -v
|
||||
```
|
||||
- `-v` provides verbose output.
|
||||
- Tests are in `tests/test_app.py`, covering 27 cases.
|
||||
|
||||
### Test Coverage
|
||||
Test Coverage
|
||||
|
||||
Tests include:
|
||||
- **UI Pages**:
|
||||
- Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG.
|
||||
- Form rendering, submissions, and error handling (e.g., invalid JSON, CSRF).
|
||||
- **API Endpoints**:
|
||||
- `POST /api/import-ig`: Success, invalid key, duplicates, profile relationships.
|
||||
- `POST /api/push-ig`: Success, validation, errors.
|
||||
- `GET /get-structure`, `GET /get-example`: Success and failure cases.
|
||||
- **Database**:
|
||||
- Processing, unloading, and viewing IGs.
|
||||
- **File Operations**:
|
||||
- Package processing, deletion.
|
||||
- **Security**:
|
||||
- CSRF protection, flash messages, secret key.
|
||||
UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter.
|
||||
API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example.
|
||||
Database: IG processing, unloading, viewing.
|
||||
File Operations: Package processing, deletion, FSH output.
|
||||
Security: CSRF protection, flash messages, secret key.
|
||||
FSH Converter: Form submission, file/text input, GoFSH execution, Fishing Trip comparison.
|
||||
|
||||
### Example Test Output
|
||||
|
||||
```
|
||||
Example Test Output
|
||||
================================================================ test session starts =================================================================
|
||||
platform linux -- Python 3.9.22, pytest-8.3.5, pluggy-1.5.0
|
||||
platform linux -- Python 3.12, pytest-8.3.5, pluggy-1.5.0
|
||||
rootdir: /app/tests
|
||||
collected 27 items
|
||||
|
||||
test_app.py::TestFHIRFlareIGToolkit::test_homepage PASSED [ 3%]
|
||||
test_app.py::TestFHIRFlareIGToolkit::test_import_ig_page PASSED [ 7%]
|
||||
test_app.py::TestFHIRFlareIGToolkit::test_fsh_converter_page PASSED [ 11%]
|
||||
...
|
||||
test_app.py::TestFHIRFlareIGToolkit::test_validate_sample_page PASSED [ 85%]
|
||||
test_app.py::TestFHIRFlareIGToolkit::test_validate_sample_success PASSED [ 88%]
|
||||
...
|
||||
============================================================= 27 passed in 1.23s ==============================================================
|
||||
```
|
||||
|
||||
### Troubleshooting Tests
|
||||
Troubleshooting Tests
|
||||
|
||||
- **ModuleNotFoundError**: Ensure `app.py`, `services.py`, and `forms.py` are in `/app/`. Run tests from the project root.
|
||||
- **TemplateNotFound**: Verify templates (`validate_sample.html`, etc.) are in `/app/templates/`.
|
||||
- **Database Errors**: Ensure `instance/fhir_ig.db` is writable (`chmod 777 instance`).
|
||||
- **Mock Failures**: Check `tests/test_app.py` for correct mocking of `services.py` functions.
|
||||
ModuleNotFoundError: Ensure app.py, services.py, forms.py are in /app/.
|
||||
TemplateNotFound: Verify templates are in /app/templates/.
|
||||
Database Errors: Ensure instance/fhir_ig.db is writable (chmod 777 instance).
|
||||
Mock Failures: Check tests/test_app.py for correct mocking.
|
||||
|
||||
## Development Notes
|
||||
Development Notes
|
||||
Background
|
||||
The toolkit addresses the need for a comprehensive FHIR IG management tool, with recent enhancements for resource validation, FSH conversion with advanced GoFSH features, and flexible versioning, making it a versatile platform for FHIR developers.
|
||||
Technical Decisions
|
||||
|
||||
### Background
|
||||
Flask: Lightweight and flexible for web development.
|
||||
SQLite: Simple for development; consider PostgreSQL for production.
|
||||
Bootstrap 5.3.3: Responsive UI with custom styling for duplicates, FSH output, and waiting spinner.
|
||||
Lottie-Web: Renders themed animations for FSH conversion waiting spinner.
|
||||
GoFSH/SUSHI: Integrated via Node.js for advanced FSH conversion and round-trip validation.
|
||||
Docker: Ensures consistent deployment with Flask and HAPI FHIR.
|
||||
Flexible Versioning: Supports non-standard IG versions (e.g., -preview, -ballot).
|
||||
Live Console: Real-time feedback for complex operations.
|
||||
Validation: Alpha feature with ongoing FHIRPath improvements.
|
||||
|
||||
The toolkit addresses the need for a user-friendly FHIR IG management tool, with recent enhancements for resource validation and flexible version handling (e.g., `1.1.0-preview`).
|
||||
Recent Updates
|
||||
|
||||
### Technical Decisions
|
||||
Waiting Spinner for FSH Converter (April 2025):
|
||||
Added a themed (light/dark) Lottie animation spinner during FSH execution to indicate processing.
|
||||
Path: templates/fsh_converter.html, static/animations/loading-dark.json, static/animations/loading-light.json, static/js/lottie-web.min.js.
|
||||
|
||||
- **Flask**: Lightweight and flexible for web development.
|
||||
- **SQLite**: Simple for development; consider PostgreSQL for production.
|
||||
- **Bootstrap 5**: Responsive UI with custom CSS for duplicate highlighting.
|
||||
- **Flask-WTF**: Robust form validation and CSRF protection.
|
||||
- **Docker**: Ensures consistent deployment.
|
||||
- **Flexible Versioning**: Supports non-standard version formats for FHIR IGs (e.g., `-preview`, `-ballot`).
|
||||
- **Validation**: Alpha feature for validating FHIR resources/bundles, with ongoing improvements to FHIRPath handling.
|
||||
|
||||
### Recent Updates
|
||||
Advanced FSH Converter (April 2025):
|
||||
Added support for GoFSH advanced options: --fshing-trip (round-trip validation with SUSHI), --dependency (additional packages), --indent (indented rules), --meta-profile (only-one, first, none), --alias-file (custom aliases), --no-alias (disable alias generation).
|
||||
Displays Fishing Trip comparison reports via a badge button.
|
||||
Path: templates/fsh_converter.html, app.py, services.py, forms.py.
|
||||
|
||||
- **Version Format Support**: Added support for flexible IG version formats (e.g., `1.1.0-preview`, `1.1.2-ballot`, `current`) in `forms.py`.
|
||||
- **CSRF Protection**: Fixed missing CSRF tokens in `cp_downloaded_igs.html` and `cp_push_igs.html`, ensuring secure form submissions.
|
||||
- **Form Handling**: Updated `validate_sample.html` and `app.py` to use `version` instead of `package_version`, aligning with `ValidationForm`.
|
||||
- **Validation Feature**: Added alpha support for validating FHIR resources/bundles against IGs, with error/warning reports (UI only).
|
||||
|
||||
### Known Issues and Workarounds
|
||||
FSH Converter (April 2025):
|
||||
Added /fsh-converter page for FHIR to FSH conversion using GoFSH.
|
||||
Path: templates/fsh_converter.html, app.py, services.py, forms.py.
|
||||
|
||||
- **CSRF Errors**: Ensure `SECRET_KEY` is set and forms include `{{ form.csrf_token }}`. Check logs for `flask_wtf.csrf` errors.
|
||||
- **Version Validation**: Previously restricted to `x.y.z`; now supports suffixes like `-preview`. Report any import issues.
|
||||
- **Validation Accuracy**: Resource validation is alpha; FHIRPath logic may miss complex constraints. Report anomalies to GitHub (remove PHI).
|
||||
- **Package Parsing**: Non-standard `.tgz` filenames may parse incorrectly. Fallback treats them as name-only packages.
|
||||
- **Permissions**: Ensure `instance` directory is writable (`chmod -R 777 instance`) to avoid database or file errors.
|
||||
|
||||
### Future Improvements
|
||||
Favicon Fix (April 2025):
|
||||
Resolved 404 for /favicon.ico on /fsh-converter by ensuring static/favicon.ico is served.
|
||||
Added fallback /favicon.ico route in app.py.
|
||||
|
||||
- [ ] **Sorting Versions**: Sort package versions in `/view-igs` (e.g., ascending).
|
||||
- [ ] **Duplicate Resolution**: Add options to keep latest version or merge resources.
|
||||
- [ ] **Production Database**: Support PostgreSQL for scalability.
|
||||
- [ ] **Validation Enhancements**: Improve FHIRPath handling for complex constraints; add API endpoint for validation.
|
||||
- [ ] **Error Reporting**: Enhance UI feedback for validation errors with specific element paths.
|
||||
|
||||
**Completed Items**:
|
||||
- ~~Testing: Comprehensive test suite for UI, API, and database.~~
|
||||
- ~~Inbound API: `POST /api/import-ig` with dependency and profile support.~~
|
||||
- ~~Outbound API: `POST /api/push-ig` with validation and feedback.~~
|
||||
- ~~Flexible Versioning: Support for `-preview`, `-ballot`, etc.~~
|
||||
- ~~CSRF Fixes: Secured forms in `cp_downloaded_igs.html`, `cp_push_igs.html`.~~
|
||||
- ~~Resource Validation: UI for validating resources/bundles (alpha).~~
|
||||
Menu Item (April 2025):
|
||||
Added “FSH Converter” to the navbar in base.html.
|
||||
|
||||
### Far-Distant Improvements
|
||||
|
||||
- **Cache Service**: Use Redis to cache IG metadata for faster queries.
|
||||
- **Database Optimization**: Add composite index on `ProcessedIg.package_name` and `ProcessedIg.version` for efficient lookups.
|
||||
UPLOAD_FOLDER Fix (April 2025):
|
||||
Fixed 500 error on /fsh-converter by setting app.config['UPLOAD_FOLDER'] = '/app/static/uploads'.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `app.py`: Main Flask application.
|
||||
- `services.py`: Logic for IG import, processing, validation, and pushing.
|
||||
- `forms.py`: Form definitions for import and validation.
|
||||
- `templates/`: HTML templates (`validate_sample.html`, `cp_downloaded_igs.html`, etc.).
|
||||
- `instance/`: SQLite database (`fhir_ig.db`) and packages (`fhir_packages/`).
|
||||
- `tests/test_app.py`: Test suite with 27 cases.
|
||||
- `requirements.txt`: Python dependencies.
|
||||
- `Dockerfile`: Docker configuration.
|
||||
Validation (April 2025):
|
||||
Alpha support for validating resources/bundles in /validate-sample.
|
||||
Path: templates/validate_sample.html, app.py, services.py.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! To contribute:
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch (`git checkout -b feature/your-feature`).
|
||||
3. Commit changes (`git commit -m "Add your feature"`).
|
||||
4. Push to your branch (`git push origin feature/your-feature`).
|
||||
5. Open a Pull Request.
|
||||
CSRF Protection: Fixed missing CSRF tokens in cp_downloaded_igs.html, cp_push_igs.html.
|
||||
Version Support: Added flexible version formats (e.g., 1.1.0-preview) in forms.py.
|
||||
|
||||
Ensure code follows style guidelines and includes tests.
|
||||
Known Issues and Workarounds
|
||||
|
||||
## Troubleshooting
|
||||
Favicon 404: Clear browser cache or verify /app/static/favicon.ico:
|
||||
docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico
|
||||
|
||||
- **CSRF Errors**: Verify `SECRET_KEY` is set and forms include `{{ form.csrf_token }}`. Check browser DevTools for POST data.
|
||||
- **Import Fails**: Confirm package name/version (e.g., `hl7.fhir.au.core#1.1.0-preview`) and internet connectivity.
|
||||
- **Validation Errors**: Alpha feature; report issues to GitHub with JSON samples (remove PHI).
|
||||
- **Database Issues**: Ensure `instance/fhir_ig.db` is writable (`chmod 777 instance`).
|
||||
- **Docker Volume**: Mount `instance` directory to persist data:
|
||||
```bash
|
||||
docker run -v $(pwd)/instance:/app/instance ...
|
||||
```
|
||||
|
||||
## License
|
||||
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
|
||||
|
||||
Licensed under the Apache 2.0 License. See `LICENSE` for details.
|
||||
Import Fails: Check package name/version and connectivity.
|
||||
|
||||
Validation Accuracy: Alpha feature; FHIRPath may miss complex constraints. Report issues to GitHub (remove PHI).
|
||||
|
||||
Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
|
||||
|
||||
Permissions: Ensure instance/ and static/uploads/ are writable:
|
||||
chmod -R 777 instance static/uploads logs
|
||||
|
||||
|
||||
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation:
|
||||
docker exec -it <container_name> sushi --version
|
||||
|
||||
|
||||
|
||||
Future Improvements
|
||||
|
||||
Validation: Enhance FHIRPath for complex constraints; add API endpoint.
|
||||
Sorting: Sort IG versions in /view-igs (e.g., ascending).
|
||||
Duplicate Resolution: Options to keep latest version or merge resources.
|
||||
Production Database: Support PostgreSQL.
|
||||
Error Reporting: Detailed validation error paths in the UI.
|
||||
FSH Enhancements: Add API endpoint for FSH conversion; support inline instance construction.
|
||||
FHIR Operations: Add complex parameter support (e.g., /$diff with left/right).
|
||||
Spinner Enhancements: Customize spinner animation speed or size.
|
||||
|
||||
Completed Items
|
||||
|
||||
Testing suite with 27 cases.
|
||||
API endpoints for POST /api/import-ig and POST /api/push-ig.
|
||||
Flexible versioning (-preview, -ballot).
|
||||
CSRF fixes for forms.
|
||||
Resource validation UI (alpha).
|
||||
FSH Converter with advanced GoFSH features and waiting spinner.
|
||||
|
||||
Far-Distant Improvements
|
||||
|
||||
Cache Service: Use Redis for IG metadata caching.
|
||||
Database Optimization: Composite index on ProcessedIg.package_name and ProcessedIg.version.
|
||||
|
||||
Directory Structure
|
||||
FHIRFLARE-IG-Toolkit/
|
||||
├── app.py # Main Flask application
|
||||
├── Build and Run for first time.bat # Windows script for first-time Docker setup
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── Dockerfile # Docker configuration
|
||||
├── forms.py # Form definitions
|
||||
├── LICENSE.md # Apache 2.0 License
|
||||
├── README.md # Project documentation
|
||||
├── requirements.txt # Python dependencies
|
||||
├── Run.bat # Windows script for running Docker
|
||||
├── services.py # Logic for IG import, processing, validation, pushing, and FSH conversion
|
||||
├── supervisord.conf # Supervisor configuration
|
||||
├── hapi-fhir-Setup/
|
||||
│ ├── README.md # HAPI FHIR setup instructions
|
||||
│ └── target/
|
||||
│ └── classes/
|
||||
│ └── application.yaml # HAPI FHIR configuration
|
||||
├── instance/
|
||||
│ ├── fhir_ig.db # SQLite database
|
||||
│ ├── fhir_ig.db.old # Database backup
|
||||
│ └── fhir_packages/ # Stored IG packages and metadata
|
||||
│ ├── hl7.fhir.au.base-5.1.0-preview.metadata.json
|
||||
│ ├── hl7.fhir.au.base-5.1.0-preview.tgz
|
||||
│ ├── hl7.fhir.au.core-1.1.0-preview.metadata.json
|
||||
│ ├── hl7.fhir.au.core-1.1.0-preview.tgz
|
||||
│ ├── hl7.fhir.r4.core-4.0.1.metadata.json
|
||||
│ ├── hl7.fhir.r4.core-4.0.1.tgz
|
||||
│ ├── hl7.fhir.uv.extensions.r4-5.2.0.metadata.json
|
||||
│ ├── hl7.fhir.uv.extensions.r4-5.2.0.tgz
|
||||
│ ├── hl7.fhir.uv.ipa-1.0.0.metadata.json
|
||||
│ ├── hl7.fhir.uv.ipa-1.0.0.tgz
|
||||
│ ├── hl7.fhir.uv.smart-app-launch-2.0.0.metadata.json
|
||||
│ ├── hl7.fhir.uv.smart-app-launch-2.0.0.tgz
|
||||
│ ├── hl7.fhir.uv.smart-app-launch-2.1.0.metadata.json
|
||||
│ ├── hl7.fhir.uv.smart-app-launch-2.1.0.tgz
|
||||
│ ├── hl7.terminology.r4-5.0.0.metadata.json
|
||||
│ ├── hl7.terminology.r4-5.0.0.tgz
|
||||
│ ├── hl7.terminology.r4-6.2.0.metadata.json
|
||||
│ └── hl7.terminology.r4-6.2.0.tgz
|
||||
├── logs/
|
||||
│ ├── flask.log # Flask application logs
|
||||
│ ├── flask_err.log # Flask error logs
|
||||
│ ├── supervisord.log # Supervisor logs
|
||||
│ ├── supervisord.pid # Supervisor PID file
|
||||
│ ├── tomcat.log # Tomcat logs for HAPI FHIR
|
||||
│ └── tomcat_err.log # Tomcat error logs
|
||||
├── static/
|
||||
│ ├── animations/
|
||||
│ │ ├── loading-dark.json # Dark theme spinner animation
|
||||
│ │ └── loading-light.json # Light theme spinner animation
|
||||
│ ├── favicon.ico # Application favicon
|
||||
│ ├── FHIRFLARE.png # Application logo
|
||||
│ ├── js/
|
||||
│ │ └── lottie-web.min.js # Lottie library for spinner
|
||||
│ └── uploads/
|
||||
│ ├── output.fsh # Generated FSH output
|
||||
│ └── fsh_output/
|
||||
│ ├── sushi-config.yaml # SUSHI configuration
|
||||
│ └── input/
|
||||
│ └── fsh/
|
||||
│ ├── aliases.fsh # FSH aliases
|
||||
│ ├── index.txt # FSH index
|
||||
│ └── instances/
|
||||
│ └── banks-mia-leanne.fsh # Example FSH instance
|
||||
├── templates/
|
||||
│ ├── base.html # Base template
|
||||
│ ├── cp_downloaded_igs.html # UI for managing IGs
|
||||
│ ├── cp_push_igs.html # UI for pushing IGs
|
||||
│ ├── cp_view_processed_ig.html # UI for viewing processed IGs
|
||||
│ ├── fhir_ui.html # UI for FHIR API explorer
|
||||
│ ├── fhir_ui_operations.html # UI for FHIR server operations
|
||||
│ ├── fsh_converter.html # UI for FSH conversion
|
||||
│ ├── import_ig.html # UI for importing IGs
|
||||
│ ├── index.html # Homepage
|
||||
│ ├── validate_sample.html # UI for validating resources/bundles
|
||||
│ └── _form_helpers.html # Form helper macros
|
||||
├── tests/
|
||||
│ └── test_app.py # Test suite with 27 cases
|
||||
└── hapi-fhir-jpaserver/ # HAPI FHIR server resources
|
||||
|
||||
Contributing
|
||||
|
||||
Fork the repository.
|
||||
Create a feature branch (git checkout -b feature/your-feature).
|
||||
Commit changes (git commit -m "Add your feature").
|
||||
Push to your branch (git push origin feature/your-feature).
|
||||
Open a Pull Request.
|
||||
|
||||
Ensure code follows PEP 8 and includes tests in tests/test_app.py.
|
||||
Troubleshooting
|
||||
|
||||
Favicon 404: Clear browser cache or verify /app/static/favicon.ico:
|
||||
docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico
|
||||
|
||||
|
||||
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
|
||||
|
||||
Import Fails: Check package name/version and connectivity.
|
||||
|
||||
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
|
||||
|
||||
Package Parsing: Non-standard .tgz filenamesgrass may parse incorrectly. Fallback uses name-only parsing.
|
||||
|
||||
Permissions: Ensure instance/ and static/uploads/ are writable:
|
||||
chmod -R 777 instance static/uploads logs
|
||||
|
||||
|
||||
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation:
|
||||
docker exec -it <container_name> sushi --version
|
||||
|
||||
|
||||
|
||||
License
|
||||
Licensed under the Apache 2.0 License. See LICENSE.md for details.
|
||||
|
25
Run.bat
Normal file
25
Run.bat
Normal file
@ -0,0 +1,25 @@
|
||||
REM --- Step 1: Start Docker containers ---
|
||||
echo ===> Starting Docker containers (Step 7)...
|
||||
docker-compose up -d
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Docker Compose up failed. Check Docker installation and container configurations. ErrorLevel: %errorlevel%
|
||||
goto :error
|
||||
)
|
||||
echo Docker containers started successfully. ErrorLevel: %errorlevel%
|
||||
echo.
|
||||
|
||||
echo ====================================
|
||||
echo Script finished successfully!
|
||||
echo ====================================
|
||||
goto :eof
|
||||
|
||||
:error
|
||||
echo ------------------------------------
|
||||
echo An error occurred. Script aborted.
|
||||
echo ------------------------------------
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:eof
|
||||
echo Script execution finished.
|
||||
pause
|
484
app.py
484
app.py
@ -2,7 +2,8 @@ import sys
|
||||
import os
|
||||
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
|
||||
import datetime
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, Response, current_app
|
||||
import shutil
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, Response, current_app, session, send_file, make_response
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
@ -13,7 +14,9 @@ import requests
|
||||
import re
|
||||
import services # Restore full module import
|
||||
from services import services_bp # Keep Blueprint import
|
||||
from forms import IgImportForm, ValidationForm
|
||||
from forms import IgImportForm, ValidationForm, FSHConverterForm
|
||||
from wtforms import SubmitField
|
||||
import tempfile
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
@ -27,6 +30,19 @@ app.config['FHIR_PACKAGES_DIR'] = '/app/instance/fhir_packages'
|
||||
app.config['API_KEY'] = os.environ.get('API_KEY', 'your-fallback-api-key-here')
|
||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
||||
app.config['UPLOAD_FOLDER'] = '/app/static/uploads' # For GoFSH output
|
||||
|
||||
# <<< ADD THIS CONTEXT PROCESSOR >>>
|
||||
@app.context_processor
|
||||
def inject_app_mode():
|
||||
"""Injects the app_mode into template contexts."""
|
||||
return dict(app_mode=app.config.get('APP_MODE', 'standalone'))
|
||||
# <<< END ADD >>>
|
||||
|
||||
# Read application mode from environment variable, default to 'standalone'
|
||||
app.config['APP_MODE'] = os.environ.get('APP_MODE', 'standalone').lower()
|
||||
logger.info(f"Application running in mode: {app.config['APP_MODE']}")
|
||||
# --- END mode check ---
|
||||
|
||||
# Ensure directories exist and are writable
|
||||
instance_path = '/app/instance'
|
||||
@ -40,6 +56,7 @@ try:
|
||||
logger.debug(f"Flask instance folder path: {instance_folder_path}")
|
||||
os.makedirs(instance_folder_path, exist_ok=True)
|
||||
os.makedirs(packages_path, exist_ok=True)
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
logger.debug(f"Directories created/verified: Instance: {instance_folder_path}, Packages: {packages_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create/verify directories: {e}", exc_info=True)
|
||||
@ -188,6 +205,15 @@ def view_igs():
|
||||
site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now(),
|
||||
config=app.config)
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
"""Renders the about page."""
|
||||
# The app_mode is automatically injected by the context processor
|
||||
return render_template('about.html',
|
||||
title="About", # Optional title for the page
|
||||
site_name='FHIRFLARE IG Toolkit') # Or get from config
|
||||
|
||||
|
||||
@app.route('/push-igs', methods=['GET'])
|
||||
def push_igs():
|
||||
# form = FlaskForm() # OLD - Replace this line
|
||||
@ -376,138 +402,174 @@ def view_ig(processed_ig_id):
|
||||
config=current_app.config)
|
||||
|
||||
@app.route('/get-structure')
|
||||
def get_structure_definition():
|
||||
def get_structure():
|
||||
package_name = request.args.get('package_name')
|
||||
package_version = request.args.get('package_version')
|
||||
resource_identifier = request.args.get('resource_type')
|
||||
if not all([package_name, package_version, resource_identifier]):
|
||||
logger.warning("get_structure_definition: Missing query parameters.")
|
||||
resource_type = request.args.get('resource_type')
|
||||
# Keep view parameter for potential future use or caching, though not used directly in this revised logic
|
||||
view = request.args.get('view', 'snapshot') # Default to snapshot view processing
|
||||
|
||||
if not all([package_name, package_version, resource_type]):
|
||||
logger.warning("get_structure: Missing query parameters: package_name=%s, package_version=%s, resource_type=%s", package_name, package_version, resource_type)
|
||||
return jsonify({"error": "Missing required query parameters: package_name, package_version, resource_type"}), 400
|
||||
|
||||
packages_dir = current_app.config.get('FHIR_PACKAGES_DIR')
|
||||
if not packages_dir:
|
||||
logger.error("FHIR_PACKAGES_DIR not configured.")
|
||||
return jsonify({"error": "Server configuration error: Package directory not set."}), 500
|
||||
|
||||
tgz_filename = services.construct_tgz_filename(package_name, package_version)
|
||||
tgz_path = os.path.join(packages_dir, tgz_filename)
|
||||
sd_data = None
|
||||
fallback_used = False
|
||||
source_package_id = f"{package_name}#{package_version}"
|
||||
logger.debug(f"Attempting to find SD for '{resource_identifier}' in {tgz_filename}")
|
||||
logger.debug(f"Attempting to find SD for '{resource_type}' in {tgz_filename}")
|
||||
|
||||
# --- Fetch SD Data (Keep existing logic including fallback) ---
|
||||
if os.path.exists(tgz_path):
|
||||
try:
|
||||
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_identifier)
|
||||
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type)
|
||||
# Add error handling as before...
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting SD for '{resource_identifier}' from {tgz_path}: {e}", exc_info=True)
|
||||
logger.error(f"Unexpected error extracting SD '{resource_type}' from {tgz_path}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Unexpected error reading StructureDefinition: {str(e)}"}), 500
|
||||
else:
|
||||
logger.warning(f"Package file not found: {tgz_path}")
|
||||
# Try fallback... (keep existing fallback logic)
|
||||
if sd_data is None:
|
||||
logger.info(f"SD for '{resource_identifier}' not found in {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.")
|
||||
logger.info(f"SD for '{resource_type}' not found in {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.")
|
||||
core_package_name, core_package_version = services.CANONICAL_PACKAGE
|
||||
core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version)
|
||||
core_tgz_path = os.path.join(packages_dir, core_tgz_filename)
|
||||
if not os.path.exists(core_tgz_path):
|
||||
logger.warning(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally, attempting download.")
|
||||
# Handle missing core package / download if needed...
|
||||
logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally.")
|
||||
return jsonify({"error": f"SD for '{resource_type}' not found in primary package, and core package is missing."}), 500
|
||||
# (Add download logic here if desired)
|
||||
try:
|
||||
result = services.import_package_and_dependencies(core_package_name, core_package_version, dependency_mode='direct')
|
||||
if result['errors'] and not result['downloaded']:
|
||||
err_msg = f"Failed to download fallback core package {services.CANONICAL_PACKAGE_ID}: {result['errors'][0]}"
|
||||
logger.error(err_msg)
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found in primary package, and failed to download core package: {result['errors'][0]}"}), 500
|
||||
elif not os.path.exists(core_tgz_path):
|
||||
err_msg = f"Core package download reported success but file {core_tgz_filename} still not found."
|
||||
logger.error(err_msg)
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found, and core package download failed unexpectedly."}), 500
|
||||
else:
|
||||
logger.info(f"Successfully downloaded core package {services.CANONICAL_PACKAGE_ID}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading core package {services.CANONICAL_PACKAGE_ID}: {str(e)}", exc_info=True)
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found, and error downloading core package: {str(e)}"}), 500
|
||||
if os.path.exists(core_tgz_path):
|
||||
try:
|
||||
sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_identifier)
|
||||
sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_type)
|
||||
if sd_data is not None:
|
||||
fallback_used = True
|
||||
source_package_id = services.CANONICAL_PACKAGE_ID
|
||||
logger.info(f"Found SD for '{resource_identifier}' in fallback package {source_package_id}.")
|
||||
else:
|
||||
logger.error(f"SD for '{resource_identifier}' not found in primary package OR fallback {services.CANONICAL_PACKAGE_ID}.")
|
||||
return jsonify({"error": f"StructureDefinition for '{resource_identifier}' not found in {package_name}#{package_version} or in core FHIR package."}), 404
|
||||
logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}.")
|
||||
# Add error handling as before...
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting SD for '{resource_identifier}' from fallback {core_tgz_path}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Error reading core FHIR package: {str(e)}"}), 500
|
||||
logger.error(f"Unexpected error extracting SD '{resource_type}' from fallback {core_tgz_path}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Unexpected error reading fallback StructureDefinition: {str(e)}"}), 500
|
||||
|
||||
# --- Check if SD data was found ---
|
||||
if not sd_data:
|
||||
logger.error(f"SD for '{resource_type}' not found in primary or fallback package.")
|
||||
return jsonify({"error": f"StructureDefinition for '{resource_type}' not found."}), 404
|
||||
|
||||
# --- *** START Backend Modification *** ---
|
||||
# Extract snapshot and differential elements
|
||||
snapshot_elements = sd_data.get('snapshot', {}).get('element', [])
|
||||
differential_elements = sd_data.get('differential', {}).get('element', [])
|
||||
|
||||
# Create a set of element IDs from the differential for efficient lookup
|
||||
# Using element 'id' is generally more reliable than 'path' for matching
|
||||
differential_ids = {el.get('id') for el in differential_elements if el.get('id')}
|
||||
logger.debug(f"Found {len(differential_ids)} unique IDs in differential.")
|
||||
|
||||
enriched_elements = []
|
||||
if snapshot_elements:
|
||||
logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.")
|
||||
for element in snapshot_elements:
|
||||
element_id = element.get('id')
|
||||
# Add the isInDifferential flag
|
||||
element['isInDifferential'] = bool(element_id and element_id in differential_ids)
|
||||
enriched_elements.append(element)
|
||||
# Clean narrative from enriched elements (should have been done in find_and_extract_sd, but double check)
|
||||
enriched_elements = [services.remove_narrative(el) for el in enriched_elements]
|
||||
else:
|
||||
logger.error(f"Core package {core_tgz_path} missing even after download attempt.")
|
||||
return jsonify({"error": f"SD not found, and core package could not be located/downloaded."}), 500
|
||||
elements = sd_data.get('snapshot', {}).get('element', [])
|
||||
if not elements and 'differential' in sd_data:
|
||||
logger.debug(f"Using differential elements for {resource_identifier} as snapshot is missing.")
|
||||
elements = sd_data.get('differential', {}).get('element', [])
|
||||
if not elements:
|
||||
logger.warning(f"No snapshot or differential elements found in the SD for '{resource_identifier}' from {source_package_id}")
|
||||
# Fallback: If no snapshot, log warning. Maybe return differential only?
|
||||
# Returning only differential might break frontend filtering logic.
|
||||
# For now, return empty, but log clearly.
|
||||
logger.warning(f"No snapshot found for {resource_type} in {source_package_id}. Returning empty element list.")
|
||||
enriched_elements = [] # Or consider returning differential and handle in JS
|
||||
|
||||
# --- *** END Backend Modification *** ---
|
||||
|
||||
# Retrieve must_support_paths from DB (keep existing logic)
|
||||
must_support_paths = []
|
||||
processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
||||
if processed_ig and processed_ig.must_support_elements:
|
||||
must_support_paths = processed_ig.must_support_elements.get(resource_identifier, [])
|
||||
logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths for '{resource_identifier}' from processed IG {package_name}#{package_version}")
|
||||
# Use the profile ID (which is likely the resource_type for profiles) as the key
|
||||
must_support_paths = processed_ig.must_support_elements.get(resource_type, [])
|
||||
logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths for '{resource_type}' from processed IG DB record.")
|
||||
else:
|
||||
logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}, resource {resource_type}")
|
||||
|
||||
|
||||
# Construct the response
|
||||
response_data = {
|
||||
"elements": elements,
|
||||
"must_support_paths": must_support_paths,
|
||||
"fallback_used": fallback_used,
|
||||
"source_package": source_package_id,
|
||||
"requested_identifier": resource_identifier,
|
||||
"original_package": f"{package_name}#{package_version}"
|
||||
'elements': enriched_elements, # Return the processed list
|
||||
'must_support_paths': must_support_paths,
|
||||
'fallback_used': fallback_used,
|
||||
'source_package': source_package_id
|
||||
# Removed raw structure_definition to reduce payload size, unless needed elsewhere
|
||||
}
|
||||
return jsonify(response_data)
|
||||
|
||||
# Use Response object for consistent JSON formatting
|
||||
return Response(json.dumps(response_data, indent=2), mimetype='application/json') # Use indent=2 for readability if debugging
|
||||
|
||||
@app.route('/get-example')
|
||||
def get_example_content():
|
||||
def get_example():
|
||||
package_name = request.args.get('package_name')
|
||||
package_version = request.args.get('package_version')
|
||||
example_member_path = request.args.get('filename')
|
||||
if not all([package_name, package_version, example_member_path]):
|
||||
logger.warning(f"get_example_content: Missing query parameters: name={package_name}, version={package_version}, path={example_member_path}")
|
||||
version = request.args.get('package_version')
|
||||
filename = request.args.get('filename')
|
||||
if not all([package_name, version, filename]):
|
||||
logger.warning("get_example: Missing query parameters: package_name=%s, version=%s, filename=%s", package_name, version, filename)
|
||||
return jsonify({"error": "Missing required query parameters: package_name, package_version, filename"}), 400
|
||||
if not example_member_path.startswith('package/') or '..' in example_member_path:
|
||||
logger.warning(f"Invalid example file path requested: {example_member_path}")
|
||||
if not filename.startswith('package/') or '..' in filename:
|
||||
logger.warning(f"Invalid example file path requested: {filename}")
|
||||
return jsonify({"error": "Invalid example file path."}), 400
|
||||
packages_dir = current_app.config.get('FHIR_PACKAGES_DIR')
|
||||
if not packages_dir:
|
||||
logger.error("FHIR_PACKAGES_DIR not configured.")
|
||||
return jsonify({"error": "Server configuration error: Package directory not set."}), 500
|
||||
tgz_filename = services.construct_tgz_filename(package_name, package_version)
|
||||
tgz_filename = services.construct_tgz_filename(package_name, version)
|
||||
tgz_path = os.path.join(packages_dir, tgz_filename)
|
||||
if not os.path.exists(tgz_path):
|
||||
logger.error(f"Package file not found for example extraction: {tgz_path}")
|
||||
return jsonify({"error": f"Package file not found: {package_name}#{package_version}"}), 404
|
||||
logger.error(f"Package file not found: {tgz_path}")
|
||||
return jsonify({"error": f"Package {package_name}#{version} not found"}), 404
|
||||
try:
|
||||
with tarfile.open(tgz_path, "r:gz") as tar:
|
||||
try:
|
||||
example_member = tar.getmember(example_member_path)
|
||||
example_member = tar.getmember(filename)
|
||||
with tar.extractfile(example_member) as example_fileobj:
|
||||
content_bytes = example_fileobj.read()
|
||||
content_string = content_bytes.decode('utf-8-sig')
|
||||
content_type = 'application/json' if example_member_path.lower().endswith('.json') else \
|
||||
'application/xml' if example_member_path.lower().endswith('.xml') else \
|
||||
'text/plain'
|
||||
return Response(content_string, mimetype=content_type)
|
||||
# Parse JSON to remove narrative
|
||||
content = json.loads(content_string)
|
||||
if 'text' in content:
|
||||
logger.debug(f"Removing narrative text from example '{filename}'")
|
||||
del content['text']
|
||||
# Return filtered JSON content as a compact string
|
||||
filtered_content_string = json.dumps(content, separators=(',', ':'), sort_keys=False)
|
||||
return Response(filtered_content_string, mimetype='application/json')
|
||||
except KeyError:
|
||||
logger.error(f"Example file '{example_member_path}' not found within {tgz_filename}")
|
||||
return jsonify({"error": f"Example file '{os.path.basename(example_member_path)}' not found in package."}), 404
|
||||
except tarfile.TarError as e:
|
||||
logger.error(f"TarError reading example {example_member_path} from {tgz_filename}: {e}")
|
||||
return jsonify({"error": f"Error reading package archive: {e}"}), 500
|
||||
logger.error(f"Example file '{filename}' not found within {tgz_filename}")
|
||||
return jsonify({"error": f"Example file '{os.path.basename(filename)}' not found in package."}), 404
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON parsing error for example '{filename}' in {tgz_filename}: {e}")
|
||||
return jsonify({"error": f"Invalid JSON in example file: {str(e)}"}), 500
|
||||
except UnicodeDecodeError as e:
|
||||
logger.error(f"Encoding error reading example {example_member_path} from {tgz_filename}: {e}")
|
||||
return jsonify({"error": f"Error decoding example file (invalid UTF-8?): {e}"}), 500
|
||||
logger.error(f"Encoding error reading example '{filename}' from {tgz_filename}: {e}")
|
||||
return jsonify({"error": f"Error decoding example file (invalid UTF-8?): {str(e)}"}), 500
|
||||
except tarfile.TarError as e:
|
||||
logger.error(f"TarError reading example '{filename}' from {tgz_filename}: {e}")
|
||||
return jsonify({"error": f"Error reading package archive: {str(e)}"}), 500
|
||||
except tarfile.TarError as e:
|
||||
logger.error(f"Error opening package file {tgz_path}: {e}")
|
||||
return jsonify({"error": f"Error reading package archive: {e}"}), 500
|
||||
return jsonify({"error": f"Error reading package archive: {str(e)}"}), 500
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Package file disappeared: {tgz_path}")
|
||||
return jsonify({"error": f"Package file not found: {package_name}#{package_version}"}), 404
|
||||
return jsonify({"error": f"Package file not found: {package_name}#{version}"}), 404
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error getting example {example_member_path} from {tgz_filename}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500
|
||||
logger.error(f"Unexpected error getting example '{filename}' from {tgz_filename}: {e}", exc_info=True)
|
||||
return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
|
||||
|
||||
@app.route('/get-package-metadata')
|
||||
def get_package_metadata():
|
||||
@ -869,7 +931,7 @@ def validate_sample():
|
||||
packages=packages,
|
||||
validation_report=None,
|
||||
site_name='FHIRFLARE IG Toolkit',
|
||||
now=datetime.datetime.now()
|
||||
now=datetime.datetime.now(), app_mode=app.config['APP_MODE']
|
||||
)
|
||||
|
||||
# Exempt specific API views defined directly on 'app'
|
||||
@ -891,5 +953,271 @@ def create_db():
|
||||
with app.app_context():
|
||||
create_db()
|
||||
|
||||
|
||||
class FhirRequestForm(FlaskForm):
|
||||
submit = SubmitField('Send Request')
|
||||
|
||||
@app.route('/fhir')
|
||||
def fhir_ui():
|
||||
form = FhirRequestForm()
|
||||
return render_template('fhir_ui.html', form=form, site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now(), app_mode=app.config['APP_MODE'])
|
||||
|
||||
@app.route('/fhir-ui-operations')
|
||||
def fhir_ui_operations():
|
||||
form = FhirRequestForm()
|
||||
return render_template('fhir_ui_operations.html', form=form, site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now(), app_mode=app.config['APP_MODE'])
|
||||
|
||||
@app.route('/fhir/<path:subpath>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
def proxy_hapi(subpath):
|
||||
# Clean subpath to remove r4/, fhir/, leading/trailing slashes
|
||||
clean_subpath = subpath.replace('r4/', '').replace('fhir/', '').strip('/')
|
||||
hapi_url = f"http://localhost:8080/fhir/{clean_subpath}" if clean_subpath else "http://localhost:8080/fhir"
|
||||
headers = {k: v for k, v in request.headers.items() if k != 'Host'}
|
||||
logger.debug(f"Proxying request: {request.method} {hapi_url}")
|
||||
try:
|
||||
response = requests.request(
|
||||
method=request.method,
|
||||
url=hapi_url,
|
||||
headers=headers,
|
||||
data=request.get_data(),
|
||||
cookies=request.cookies,
|
||||
allow_redirects=False,
|
||||
timeout=5
|
||||
)
|
||||
response.raise_for_status()
|
||||
# Strip hop-by-hop headers to avoid chunked encoding issues
|
||||
response_headers = {
|
||||
k: v for k, v in response.headers.items()
|
||||
if k.lower() not in (
|
||||
'transfer-encoding', 'connection', 'content-encoding',
|
||||
'content-length', 'keep-alive', 'proxy-authenticate',
|
||||
'proxy-authorization', 'te', 'trailers', 'upgrade'
|
||||
)
|
||||
}
|
||||
response_headers['Content-Length'] = str(len(response.content))
|
||||
logger.debug(f"HAPI response: {response.status_code} {response.reason}")
|
||||
return response.content, response.status_code, response_headers.items()
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"HAPI proxy error for {subpath}: {str(e)}")
|
||||
error_message = "HAPI FHIR server is unavailable. Please check server status."
|
||||
if clean_subpath == 'metadata':
|
||||
error_message = "Unable to connect to HAPI FHIR server for status check. Local validation will be used."
|
||||
return jsonify({'error': error_message, 'details': str(e)}), 503
|
||||
|
||||
|
||||
@app.route('/api/load-ig-to-hapi', methods=['POST'])
|
||||
def load_ig_to_hapi():
|
||||
data = request.get_json()
|
||||
package_name = data.get('package_name')
|
||||
version = data.get('version')
|
||||
tgz_path = os.path.join(current_app.config['FHIR_PACKAGES_DIR'], construct_tgz_filename(package_name, version))
|
||||
if not os.path.exists(tgz_path):
|
||||
return jsonify({"error": "Package not found"}), 404
|
||||
try:
|
||||
with tarfile.open(tgz_path, "r:gz") as tar:
|
||||
for member in tar.getmembers():
|
||||
if member.name.endswith('.json') and member.name not in ['package/package.json', 'package/.index.json']:
|
||||
resource = json.load(tar.extractfile(member))
|
||||
resource_type = resource.get('resourceType')
|
||||
resource_id = resource.get('id')
|
||||
if resource_type and resource_id:
|
||||
response = requests.put(
|
||||
f"http://localhost:8080/fhir/{resource_type}/{resource_id}",
|
||||
json=resource,
|
||||
headers={'Content-Type': 'application/fhir+json'}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return jsonify({"status": "success", "message": f"Loaded {package_name}#{version} to HAPI"})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load IG to HAPI: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# Assuming 'app' and 'logger' are defined, and other necessary imports are present above
|
||||
|
||||
@app.route('/fsh-converter', methods=['GET', 'POST'])
|
||||
def fsh_converter():
|
||||
form = FSHConverterForm()
|
||||
fsh_output = None
|
||||
error = None
|
||||
comparison_report = None
|
||||
|
||||
# --- Populate package choices ---
|
||||
packages = []
|
||||
packages_dir = app.config.get('FHIR_PACKAGES_DIR', '/app/instance/fhir_packages') # Use .get with default
|
||||
logger.debug(f"Scanning packages directory: {packages_dir}")
|
||||
if os.path.exists(packages_dir):
|
||||
tgz_files = [f for f in os.listdir(packages_dir) if f.endswith('.tgz')]
|
||||
logger.debug(f"Found {len(tgz_files)} .tgz files: {tgz_files}")
|
||||
for filename in tgz_files:
|
||||
package_file_path = os.path.join(packages_dir, filename)
|
||||
try:
|
||||
# Check if it's a valid tar.gz file before opening
|
||||
if not tarfile.is_tarfile(package_file_path):
|
||||
logger.warning(f"Skipping non-tarfile or corrupted file: {filename}")
|
||||
continue
|
||||
|
||||
with tarfile.open(package_file_path, 'r:gz') as tar:
|
||||
# Find package.json case-insensitively and handle potential path variations
|
||||
package_json_path = next((m for m in tar.getmembers() if m.name.lower().endswith('package.json') and m.isfile() and ('/' not in m.name.replace('package/','', 1).lower())), None) # Handle package/ prefix better
|
||||
|
||||
if package_json_path:
|
||||
package_json_stream = tar.extractfile(package_json_path)
|
||||
if package_json_stream:
|
||||
try:
|
||||
pkg_info = json.load(package_json_stream)
|
||||
name = pkg_info.get('name')
|
||||
version = pkg_info.get('version')
|
||||
if name and version:
|
||||
package_id = f"{name}#{version}"
|
||||
packages.append((package_id, package_id))
|
||||
logger.debug(f"Added package: {package_id}")
|
||||
else:
|
||||
logger.warning(f"Missing name or version in {filename}/package.json: name={name}, version={version}")
|
||||
except json.JSONDecodeError as json_e:
|
||||
logger.warning(f"Error decoding package.json from {filename}: {json_e}")
|
||||
except Exception as read_e:
|
||||
logger.warning(f"Error reading stream from package.json in {filename}: {read_e}")
|
||||
finally:
|
||||
package_json_stream.close() # Ensure stream is closed
|
||||
else:
|
||||
logger.warning(f"Could not extract package.json stream from {filename} (path: {package_json_path.name})")
|
||||
else:
|
||||
logger.warning(f"No suitable package.json found in {filename}")
|
||||
except tarfile.ReadError as tar_e:
|
||||
logger.warning(f"Tarfile read error for {filename}: {tar_e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing package {filename}: {str(e)}")
|
||||
continue # Continue to next file
|
||||
else:
|
||||
logger.warning(f"Packages directory does not exist: {packages_dir}")
|
||||
|
||||
unique_packages = sorted(list(set(packages)), key=lambda x: x[0])
|
||||
form.package.choices = [('', 'None')] + unique_packages
|
||||
logger.debug(f"Set package choices: {form.package.choices}")
|
||||
# --- End package choices ---
|
||||
|
||||
if form.validate_on_submit(): # This block handles POST requests
|
||||
input_mode = form.input_mode.data
|
||||
# Use request.files.get to safely access file data
|
||||
fhir_file_storage = request.files.get(form.fhir_file.name)
|
||||
fhir_file = fhir_file_storage if fhir_file_storage and fhir_file_storage.filename != '' else None
|
||||
|
||||
fhir_text = form.fhir_text.data
|
||||
|
||||
alias_file_storage = request.files.get(form.alias_file.name)
|
||||
alias_file = alias_file_storage if alias_file_storage and alias_file_storage.filename != '' else None
|
||||
|
||||
output_style = form.output_style.data
|
||||
log_level = form.log_level.data
|
||||
fhir_version = form.fhir_version.data if form.fhir_version.data != 'auto' else None
|
||||
fishing_trip = form.fishing_trip.data
|
||||
dependencies = [dep.strip() for dep in form.dependencies.data.splitlines() if dep.strip()] if form.dependencies.data else None # Use splitlines()
|
||||
indent_rules = form.indent_rules.data
|
||||
meta_profile = form.meta_profile.data
|
||||
no_alias = form.no_alias.data
|
||||
|
||||
logger.debug(f"Processing input: mode={input_mode}, has_file={bool(fhir_file)}, has_text={bool(fhir_text)}, has_alias={bool(alias_file)}")
|
||||
# Pass the FileStorage object directly if needed by process_fhir_input
|
||||
input_file, temp_dir, alias_path, input_error = services.process_fhir_input(input_mode, fhir_file, fhir_text, alias_file)
|
||||
|
||||
if input_error:
|
||||
error = input_error
|
||||
flash(error, 'error')
|
||||
logger.error(f"Input processing error: {error}")
|
||||
if temp_dir and os.path.exists(temp_dir):
|
||||
try: shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
except Exception as cleanup_e: logger.warning(f"Error removing temp dir after input error {temp_dir}: {cleanup_e}")
|
||||
else:
|
||||
# Proceed only if input processing was successful
|
||||
output_dir = os.path.join(app.config.get('UPLOAD_FOLDER', '/app/static/uploads'), 'fsh_output') # Use .get
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
logger.debug(f"Running GoFSH with input: {input_file}, output_dir: {output_dir}")
|
||||
# Pass form data directly to run_gofsh
|
||||
fsh_output, comparison_report, gofsh_error = services.run_gofsh(
|
||||
input_file, output_dir, output_style, log_level, fhir_version,
|
||||
fishing_trip, dependencies, indent_rules, meta_profile, alias_path, no_alias
|
||||
)
|
||||
# Clean up temp dir after GoFSH run
|
||||
if temp_dir and os.path.exists(temp_dir):
|
||||
try:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
logger.debug(f"Successfully removed temp directory: {temp_dir}")
|
||||
except Exception as cleanup_e:
|
||||
logger.warning(f"Error removing temp directory {temp_dir}: {cleanup_e}")
|
||||
|
||||
if gofsh_error:
|
||||
error = gofsh_error
|
||||
flash(error, 'error')
|
||||
logger.error(f"GoFSH error: {error}")
|
||||
else:
|
||||
# Store potentially large output carefully - session might have limits
|
||||
session['fsh_output'] = fsh_output
|
||||
flash('Conversion successful!', 'success')
|
||||
logger.info("FSH conversion successful")
|
||||
|
||||
# Return response for POST (AJAX or full page)
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
logger.debug("Returning partial HTML for AJAX POST request.")
|
||||
return render_template('_fsh_output.html', form=form, error=error, fsh_output=fsh_output, comparison_report=comparison_report)
|
||||
else:
|
||||
# For standard POST, re-render the full page with results/errors
|
||||
logger.debug("Handling standard POST request, rendering full page.")
|
||||
return render_template('fsh_converter.html', form=form, error=error, fsh_output=fsh_output, comparison_report=comparison_report, site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
||||
|
||||
# --- Handle GET request (Initial Page Load or Failed POST Validation) ---
|
||||
else:
|
||||
if request.method == 'POST': # POST but validation failed
|
||||
logger.warning("POST request failed form validation.")
|
||||
# Render the full page, WTForms errors will be displayed by render_field
|
||||
return render_template('fsh_converter.html', form=form, error="Form validation failed. Please check fields.", fsh_output=None, comparison_report=None, site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
||||
else:
|
||||
# This is the initial GET request
|
||||
logger.debug("Handling GET request for FSH converter page.")
|
||||
# **** FIX APPLIED HERE ****
|
||||
# Make the response object to add headers
|
||||
response = make_response(render_template(
|
||||
'fsh_converter.html',
|
||||
form=form, # Pass the empty form
|
||||
error=None,
|
||||
fsh_output=None,
|
||||
comparison_report=None,
|
||||
site_name='FHIRFLARE IG Toolkit',
|
||||
now=datetime.datetime.now()
|
||||
))
|
||||
# Add headers to prevent caching
|
||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
return response
|
||||
# **** END OF FIX ****
|
||||
|
||||
@app.route('/download-fsh')
|
||||
def download_fsh():
|
||||
fsh_output = session.get('fsh_output')
|
||||
if not fsh_output:
|
||||
flash('No FSH output available for download.', 'error')
|
||||
return redirect(url_for('fsh_converter'))
|
||||
|
||||
temp_file = os.path.join(app.config['UPLOAD_FOLDER'], 'output.fsh')
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write(fsh_output)
|
||||
|
||||
return send_file(temp_file, as_attachment=True, download_name='output.fsh')
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_file(os.path.join(app.static_folder, 'favicon.ico'), mimetype='image/x-icon')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with app.app_context():
|
||||
logger.debug(f"Instance path configuration: {app.instance_path}")
|
||||
logger.debug(f"Database URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||
logger.debug(f"Packages path: {app.config['FHIR_PACKAGES_DIR']}")
|
||||
logger.debug(f"Flask instance folder path: {app.instance_path}")
|
||||
logger.debug(f"Directories created/verified: Instance: {app.instance_path}, Packages: {app.config['FHIR_PACKAGES_DIR']}")
|
||||
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||
db.create_all()
|
||||
logger.info("Database tables created successfully (if they didn't exist).")
|
||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
fhirflare:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
||||
volumes:
|
||||
- ./instance:/app/instance
|
||||
- ./static/uploads:/app/static/uploads
|
||||
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- FLASK_APP=app.py
|
||||
- FLASK_ENV=development
|
||||
- NODE_PATH=/usr/lib/node_modules
|
||||
- APP_MODE=lite
|
||||
command: supervisord -c /etc/supervisord.conf
|
80
forms.py
80
forms.py
@ -1,9 +1,12 @@
|
||||
# forms.py
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField
|
||||
from wtforms.validators import DataRequired, Regexp, ValidationError
|
||||
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField
|
||||
from wtforms.validators import DataRequired, Regexp, ValidationError, Optional
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
import re
|
||||
|
||||
# Existing forms (IgImportForm, ValidationForm) remain unchanged
|
||||
class IgImportForm(FlaskForm):
|
||||
package_name = StringField('Package Name', validators=[
|
||||
DataRequired(),
|
||||
@ -46,3 +49,76 @@ def validate_json(data, mode):
|
||||
raise ValidationError("Invalid JSON format.")
|
||||
except ValueError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
class FSHConverterForm(FlaskForm):
|
||||
package = SelectField('FHIR Package (Optional)', choices=[('', 'None')], validators=[Optional()])
|
||||
input_mode = SelectField('Input Mode', choices=[
|
||||
('file', 'Upload File'),
|
||||
('text', 'Paste Text')
|
||||
], validators=[DataRequired()])
|
||||
fhir_file = FileField('FHIR Resource File (JSON/XML)', validators=[Optional()])
|
||||
fhir_text = TextAreaField('FHIR Resource Text (JSON/XML)', validators=[Optional()])
|
||||
output_style = SelectField('Output Style', choices=[
|
||||
('file-per-definition', 'File per Definition'),
|
||||
('group-by-fsh-type', 'Group by FSH Type'),
|
||||
('group-by-profile', 'Group by Profile'),
|
||||
('single-file', 'Single File')
|
||||
], validators=[DataRequired()])
|
||||
log_level = SelectField('Log Level', choices=[
|
||||
('error', 'Error'),
|
||||
('warn', 'Warn'),
|
||||
('info', 'Info'),
|
||||
('debug', 'Debug')
|
||||
], validators=[DataRequired()])
|
||||
fhir_version = SelectField('FXML Version', choices=[
|
||||
('', 'Auto-detect'),
|
||||
('4.0.1', 'R4'),
|
||||
('4.3.0', 'R4B'),
|
||||
('5.0.0', 'R5')
|
||||
], validators=[Optional()])
|
||||
fishing_trip = BooleanField('Run Fishing Trip (Round-Trip Validation with SUSHI)', default=False)
|
||||
dependencies = TextAreaField('Dependencies (e.g., hl7.fhir.us.core@6.1.0)', validators=[Optional()])
|
||||
indent_rules = BooleanField('Indent Rules with Context Paths', default=False)
|
||||
meta_profile = SelectField('Meta Profile Handling', choices=[
|
||||
('only-one', 'Only One Profile (Default)'),
|
||||
('first', 'First Profile'),
|
||||
('none', 'Ignore Profiles')
|
||||
], validators=[DataRequired()])
|
||||
alias_file = FileField('Alias FSH File', validators=[Optional()])
|
||||
no_alias = BooleanField('Disable Alias Generation', default=False)
|
||||
submit = SubmitField('Convert to FSH')
|
||||
|
||||
def validate(self, extra_validators=None):
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
if self.input_mode.data == 'file' and not self.fhir_file.data:
|
||||
self.fhir_file.errors.append('File is required when input mode is Upload File.')
|
||||
return False
|
||||
if self.input_mode.data == 'text' and not self.fhir_text.data:
|
||||
self.fhir_text.errors.append('Text input is required when input mode is Paste Text.')
|
||||
return False
|
||||
if self.input_mode.data == 'text' and self.fhir_text.data:
|
||||
try:
|
||||
content = self.fhir_text.data.strip()
|
||||
if content.startswith('{'):
|
||||
json.loads(content)
|
||||
elif content.startswith('<'):
|
||||
ET.fromstring(content)
|
||||
else:
|
||||
self.fhir_text.errors.append('Text input must be valid JSON or XML.')
|
||||
return False
|
||||
except (json.JSONDecodeError, ET.ParseError):
|
||||
self.fhir_text.errors.append('Invalid JSON or XML format.')
|
||||
return False
|
||||
if self.dependencies.data:
|
||||
for dep in self.dependencies.data.split('\n'):
|
||||
dep = dep.strip()
|
||||
if dep and not re.match(r'^[a-zA-Z0-9\-\.]+@[a-zA-Z0-9\.\-]+$', dep):
|
||||
self.dependencies.errors.append(f'Invalid dependency format: {dep}. Use package@version (e.g., hl7.fhir.us.core@6.1.0).')
|
||||
return False
|
||||
if self.alias_file.data:
|
||||
content = self.alias_file.data.read().decode('utf-8')
|
||||
if not content.strip().endswith('.fsh'):
|
||||
self.alias_file.errors.append('Alias file must be a valid FSH file (.fsh).')
|
||||
return False
|
||||
return True
|
111
hapi-fhir-Setup/README.md
Normal file
111
hapi-fhir-Setup/README.md
Normal file
@ -0,0 +1,111 @@
|
||||
# Application Build and Run Guide - MANUAL STEPS
|
||||
|
||||
This guide outlines the steps to set up, build, and run the application, including the HAPI FHIR server component and the rest of the application managed via Docker Compose.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following installed on your system:
|
||||
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Maven](https://maven.apache.org/)
|
||||
* [Java Development Kit (JDK)](https://www.oracle.com/java/technologies/downloads/) (Ensure compatibility with the HAPI FHIR version)
|
||||
* [Docker](https://www.docker.com/products/docker-desktop/)
|
||||
* [Docker Compose](https://docs.docker.com/compose/install/) (Often included with Docker Desktop)
|
||||
|
||||
## Setup and Build
|
||||
|
||||
Follow these steps to clone the necessary repository and build the components.
|
||||
|
||||
### 1. Clone and Build the HAPI FHIR Server
|
||||
|
||||
First, clone the HAPI FHIR JPA Server Starter project and build the server application.
|
||||
|
||||
|
||||
# Step 1: Clone the repository
|
||||
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git hapi-fhir-jpaserver hapi-fhir-jpaserver
|
||||
|
||||
# Navigate into the cloned directory
|
||||
cd hapi-fhir-jpaserver
|
||||
|
||||
copy the folder from hapi-fhir-setup/target/classes/application.yaml to the hapi-fhir-jpaserver/target/classes/application.yaml folder created above
|
||||
|
||||
# Step 2: Build the HAPI server package (skipping tests, using 'boot' profile)
|
||||
# This creates the runnable WAR file in the 'target/' directory
|
||||
mvn clean package -DskipTests=true -Pboot
|
||||
|
||||
# Return to the parent directory (or your project root)
|
||||
cd ..
|
||||
2. Build the Rest of the Application (Docker)
|
||||
Next, build the Docker images for the remaining parts of the application as defined in your docker-compose.yml file. Run this command from the root directory where your docker-compose.yml file is located.
|
||||
|
||||
|
||||
|
||||
# Step 3: Build Docker images without using cache
|
||||
docker-compose build --no-cache
|
||||
Running the Application
|
||||
Option A: Running the Full Application (Recommended)
|
||||
Use Docker Compose to start all services, including (presumably) the HAPI FHIR server if it's configured in your docker-compose.yml. Run this from the root directory containing your docker-compose.yml.
|
||||
|
||||
|
||||
|
||||
# Step 4: Start all services defined in docker-compose.yml in detached mode
|
||||
docker-compose up -d
|
||||
Option B: Running the HAPI FHIR Server Standalone (Debugging Only)
|
||||
This method runs only the HAPI FHIR server directly using the built WAR file. Use this primarily for debugging the server in isolation.
|
||||
|
||||
|
||||
|
||||
# Navigate into the HAPI server directory where you built it
|
||||
cd hapi-fhir-jpaserver
|
||||
|
||||
# Run the WAR file directly using Java
|
||||
java -jar target/ROOT.war
|
||||
|
||||
# Note: You might need to configure ports or database connections
|
||||
# separately when running this way, depending on the application's needs.
|
||||
|
||||
# Remember to navigate back when done
|
||||
# cd ..
|
||||
Useful Docker Commands
|
||||
Here are some helpful commands for interacting with your running Docker containers:
|
||||
|
||||
Copying files from a container:
|
||||
To copy a file from a running container to your local machine's current directory:
|
||||
|
||||
|
||||
|
||||
# Syntax: docker cp <CONTAINER_ID_OR_NAME>:<PATH_IN_CONTAINER> <LOCAL_DESTINATION_PATH>
|
||||
docker cp <CONTAINER_ID>:/app/PATH/Filename.ext .
|
||||
(Replace <CONTAINER_ID>, /app/PATH/Filename.ext with actual values. . refers to the current directory on your host machine.)
|
||||
|
||||
Accessing a container's shell:
|
||||
To get an interactive bash shell inside a running container:
|
||||
|
||||
|
||||
|
||||
# Syntax: docker exec -it <CONTAINER_ID_OR_NAME> bash
|
||||
docker exec -it <CONTAINER_ID> bash
|
||||
(Replace <CONTAINER_ID> with the actual container ID or name. You can find this using docker ps.)
|
||||
|
||||
Viewing running containers:
|
||||
|
||||
|
||||
|
||||
docker ps
|
||||
Viewing application logs:
|
||||
|
||||
|
||||
|
||||
# Follow logs for all services
|
||||
docker-compose logs -f
|
||||
|
||||
# Follow logs for a specific service
|
||||
docker-compose logs -f <SERVICE_NAME>
|
||||
(Replace <SERVICE_NAME> with the name defined in your docker-compose.yml)
|
||||
|
||||
Stopping the application:
|
||||
To stop the services started with docker-compose up -d:
|
||||
|
||||
|
||||
|
||||
docker-compose down
|
342
hapi-fhir-Setup/target/classes/application.yaml
Normal file
342
hapi-fhir-Setup/target/classes/application.yaml
Normal file
@ -0,0 +1,342 @@
|
||||
#Uncomment the "servlet" and "context-path" lines below to make the fhir endpoint available at /example/path/fhir instead of the default value of /fhir
|
||||
server:
|
||||
# servlet:
|
||||
# context-path: /example/path
|
||||
port: 8080
|
||||
#Adds the option to go to eg. http://localhost:8080/actuator/health for seeing the running configuration
|
||||
#see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
|
||||
management:
|
||||
#The following configuration will enable the actuator endpoints at /actuator/health, /actuator/info, /actuator/prometheus, /actuator/metrics. For security purposes, only /actuator/health is enabled by default.
|
||||
endpoints:
|
||||
enabled-by-default: false
|
||||
web:
|
||||
exposure:
|
||||
include: 'health' # or e.g. 'info,health,prometheus,metrics' or '*' for all'
|
||||
endpoint:
|
||||
info:
|
||||
enabled: true
|
||||
metrics:
|
||||
enabled: true
|
||||
health:
|
||||
enabled: true
|
||||
probes:
|
||||
enabled: true
|
||||
group:
|
||||
liveness:
|
||||
include:
|
||||
- livenessState
|
||||
- readinessState
|
||||
prometheus:
|
||||
enabled: true
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
enabled: true
|
||||
spring:
|
||||
main:
|
||||
allow-circular-references: true
|
||||
flyway:
|
||||
enabled: false
|
||||
baselineOnMigrate: true
|
||||
fail-on-missing-locations: false
|
||||
datasource:
|
||||
#url: 'jdbc:h2:file:./target/database/h2'
|
||||
url: jdbc:h2:file:/app/h2-data/fhir;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE
|
||||
#url: jdbc:h2:mem:test_mem
|
||||
username: sa
|
||||
password: null
|
||||
driverClassName: org.h2.Driver
|
||||
max-active: 15
|
||||
|
||||
# database connection pool size
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
jpa:
|
||||
properties:
|
||||
hibernate.format_sql: false
|
||||
hibernate.show_sql: false
|
||||
|
||||
#Hibernate dialect is automatically detected except Postgres and H2.
|
||||
#If using H2, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
|
||||
#If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
|
||||
hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect
|
||||
# hibernate.hbm2ddl.auto: update
|
||||
# hibernate.jdbc.batch_size: 20
|
||||
# hibernate.cache.use_query_cache: false
|
||||
# hibernate.cache.use_second_level_cache: false
|
||||
# hibernate.cache.use_structured_entries: false
|
||||
# hibernate.cache.use_minimal_puts: false
|
||||
|
||||
### These settings will enable fulltext search with lucene or elastic
|
||||
hibernate.search.enabled: false
|
||||
### lucene parameters
|
||||
# hibernate.search.backend.type: lucene
|
||||
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer
|
||||
# hibernate.search.backend.directory.type: local-filesystem
|
||||
# hibernate.search.backend.directory.root: target/lucenefiles
|
||||
# hibernate.search.backend.lucene_version: lucene_current
|
||||
### elastic parameters ===> see also elasticsearch section below <===
|
||||
# hibernate.search.backend.type: elasticsearch
|
||||
# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer
|
||||
hapi:
|
||||
fhir:
|
||||
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
|
||||
### Flag is false by default, can be passed as command line argument to override.
|
||||
cr:
|
||||
enabled: false
|
||||
caregaps:
|
||||
reporter: "default"
|
||||
section_author: "default"
|
||||
cql:
|
||||
use_embedded_libraries: true
|
||||
compiler:
|
||||
### These are low-level compiler options.
|
||||
### They are not typically needed by most users.
|
||||
# validate_units: true
|
||||
# verify_only: false
|
||||
# compatibility_level: "1.5"
|
||||
error_level: Info
|
||||
signature_level: All
|
||||
# analyze_data_requirements: false
|
||||
# collapse_data_requirements: false
|
||||
# translator_format: JSON
|
||||
# enable_date_range_optimization: true
|
||||
enable_annotations: true
|
||||
enable_locators: true
|
||||
enable_results_type: true
|
||||
enable_detailed_errors: true
|
||||
# disable_list_traversal: false
|
||||
# disable_list_demotion: false
|
||||
# enable_interval_demotion: false
|
||||
# enable_interval_promotion: false
|
||||
# disable_method_invocation: false
|
||||
# require_from_keyword: false
|
||||
# disable_default_model_info_load: false
|
||||
runtime:
|
||||
debug_logging_enabled: false
|
||||
# enable_validation: false
|
||||
# enable_expression_caching: true
|
||||
terminology:
|
||||
valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT, REQUIRE, IGNORE
|
||||
valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO, USE_EXPANSION_OPERATION, PERFORM_NAIVE_EXPANSION
|
||||
valueset_membership_mode: USE_EXPANSION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_EXPANSION
|
||||
code_lookup_mode: USE_VALIDATE_CODE_OPERATION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_CODESYSTEM_URL
|
||||
data:
|
||||
search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO, USE_SEARCH_PARAMETERS, FILTER_IN_MEMORY
|
||||
terminology_parameter_mode: FILTER_IN_MEMORY # AUTO, USE_VALUE_SET_URL, USE_INLINE_CODES, FILTER_IN_MEMORY
|
||||
profile_mode: DECLARED # ENFORCED, DECLARED, OPTIONAL, TRUST, OFF
|
||||
|
||||
cdshooks:
|
||||
enabled: false
|
||||
clientIdHeaderName: client_id
|
||||
|
||||
### This enables the swagger-ui at /fhir/swagger-ui/index.html as well as the /fhir/api-docs (see https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html)
|
||||
openapi_enabled: true
|
||||
### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5
|
||||
fhir_version: R4
|
||||
### Flag is false by default. This flag enables runtime installation of IG's.
|
||||
ig_runtime_upload_enabled: false
|
||||
### This flag when enabled to true, will avail evaluate measure operations from CR Module.
|
||||
|
||||
### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers
|
||||
### to determine the FHIR server address
|
||||
# use_apache_address_strategy: false
|
||||
### forces the use of the https:// protocol for the returned server address.
|
||||
### alternatively, it may be set using the X-Forwarded-Proto header.
|
||||
# use_apache_address_strategy_https: false
|
||||
### enables the server to overwrite defaults on HTML, css, etc. under the url pattern of eg. /content/custom **
|
||||
### Folder with custom content MUST be named custom. If omitted then default content applies
|
||||
custom_content_path: ./custom
|
||||
### enables the server host custom content. If e.g. the value ./configs/app is supplied then the content
|
||||
### will be served under /web/app
|
||||
#app_content_path: ./configs/app
|
||||
### enable to set the Server URL
|
||||
# server_address: http://hapi.fhir.org/baseR4
|
||||
# defer_indexing_for_codesystems_of_size: 101
|
||||
### Flag is true by default. This flag filters resources during package installation, allowing only those resources with a valid status (e.g. active) to be installed.
|
||||
# validate_resource_status_for_package_upload: false
|
||||
# install_transitive_ig_dependencies: true
|
||||
#implementationguides:
|
||||
### example from registry (packages.fhir.org)
|
||||
# swiss:
|
||||
# name: swiss.mednet.fhir
|
||||
# version: 0.8.0
|
||||
# reloadExisting: false
|
||||
# installMode: STORE_AND_INSTALL
|
||||
# example not from registry
|
||||
# ips_1_0_0:
|
||||
# packageUrl: https://build.fhir.org/ig/HL7/fhir-ips/package.tgz
|
||||
# name: hl7.fhir.uv.ips
|
||||
# version: 1.0.0
|
||||
# supported_resource_types:
|
||||
# - Patient
|
||||
# - Observation
|
||||
##################################################
|
||||
# Allowed Bundle Types for persistence (defaults are: COLLECTION,DOCUMENT,MESSAGE)
|
||||
##################################################
|
||||
# allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
|
||||
# allow_cascading_deletes: true
|
||||
# allow_contains_searches: true
|
||||
# allow_external_references: true
|
||||
# allow_multiple_delete: true
|
||||
# allow_override_default_search_params: true
|
||||
# auto_create_placeholder_reference_targets: false
|
||||
# mass_ingestion_mode_enabled: false
|
||||
### tells the server to automatically append the current version of the target resource to references at these paths
|
||||
# auto_version_reference_at_paths: Device.patient, Device.location, Device.parent, DeviceMetric.parent, DeviceMetric.source, Observation.device, Observation.subject
|
||||
# ips_enabled: false
|
||||
# default_encoding: JSON
|
||||
# default_pretty_print: true
|
||||
# default_page_size: 20
|
||||
# delete_expunge_enabled: true
|
||||
# enable_repository_validating_interceptor: true
|
||||
# enable_index_missing_fields: false
|
||||
# enable_index_of_type: true
|
||||
# enable_index_contained_resource: false
|
||||
# upliftedRefchains_enabled: true
|
||||
# resource_dbhistory_enabled: false
|
||||
### !!Extended Lucene/Elasticsearch Indexing is still a experimental feature, expect some features (e.g. _total=accurate) to not work as expected!!
|
||||
### more information here: https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html
|
||||
advanced_lucene_indexing: false
|
||||
bulk_export_enabled: false
|
||||
bulk_import_enabled: false
|
||||
# language_search_parameter_enabled: true
|
||||
# enforce_referential_integrity_on_delete: false
|
||||
# This is an experimental feature, and does not fully support _total and other FHIR features.
|
||||
# enforce_referential_integrity_on_delete: false
|
||||
# enforce_referential_integrity_on_write: false
|
||||
# etag_support_enabled: true
|
||||
# expunge_enabled: true
|
||||
# client_id_strategy: ALPHANUMERIC
|
||||
# server_id_strategy: SEQUENTIAL_NUMERIC
|
||||
# fhirpath_interceptor_enabled: false
|
||||
# filter_search_enabled: true
|
||||
# graphql_enabled: true
|
||||
narrative_enabled: true
|
||||
mdm_enabled: false
|
||||
mdm_rules_json_location: "mdm-rules.json"
|
||||
## see: https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_server_interceptors.html#jpa-server-retry-on-version-conflicts
|
||||
# userRequestRetryVersionConflictsInterceptorEnabled : false
|
||||
# local_base_urls:
|
||||
# - https://hapi.fhir.org/baseR4
|
||||
# pre_expand_value_sets: true
|
||||
# enable_task_pre_expand_value_sets: true
|
||||
# pre_expand_value_sets_default_count: 1000
|
||||
# pre_expand_value_sets_max_count: 1000
|
||||
# maximum_expansion_size: 1000
|
||||
|
||||
logical_urls:
|
||||
- http://terminology.hl7.org/*
|
||||
- https://terminology.hl7.org/*
|
||||
- http://snomed.info/*
|
||||
- https://snomed.info/*
|
||||
- http://unitsofmeasure.org/*
|
||||
- https://unitsofmeasure.org/*
|
||||
- http://loinc.org/*
|
||||
- https://loinc.org/*
|
||||
# partitioning:
|
||||
# allow_references_across_partitions: false
|
||||
# partitioning_include_in_search_hashes: false
|
||||
# conditional_create_duplicate_identifiers_enabled: false
|
||||
cors:
|
||||
allow_Credentials: true
|
||||
# These are allowed_origin patterns, see: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#setAllowedOriginPatterns-java.util.List-
|
||||
allowed_origin:
|
||||
- '*'
|
||||
|
||||
# Search coordinator thread pool sizes
|
||||
search-coord-core-pool-size: 20
|
||||
search-coord-max-pool-size: 100
|
||||
search-coord-queue-capacity: 200
|
||||
|
||||
# Search Prefetch Thresholds.
|
||||
|
||||
# This setting sets the number of search results to prefetch. For example, if this list
|
||||
# is set to [100, 1000, -1] then the server will initially load 100 results and not
|
||||
# attempt to load more. If the user requests subsequent page(s) of results and goes
|
||||
# past 100 results, the system will load the next 900 (up to the following threshold of 1000).
|
||||
# The system will progressively work through these thresholds.
|
||||
# A threshold of -1 means to load all results. Note that if the final threshold is a
|
||||
# number other than -1, the system will never prefetch more than the given number.
|
||||
search_prefetch_thresholds: 13,503,2003,-1
|
||||
|
||||
# comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans
|
||||
#custom-bean-packages:
|
||||
|
||||
# comma-separated list of fully qualified interceptor classes.
|
||||
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
|
||||
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
|
||||
#custom-interceptor-classes:
|
||||
|
||||
# comma-separated list of fully qualified provider classes.
|
||||
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
|
||||
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
|
||||
#custom-provider-classes:
|
||||
|
||||
# Threadpool size for BATCH'ed GETs in a bundle.
|
||||
# bundle_batch_pool_size: 10
|
||||
# bundle_batch_pool_max_size: 50
|
||||
|
||||
# logger:
|
||||
# error_format: 'ERROR - ${requestVerb} ${requestUrl}'
|
||||
# format: >-
|
||||
# Path[${servletPath}] Source[${requestHeader.x-forwarded-for}]
|
||||
# Operation[${operationType} ${operationName} ${idOrResourceName}]
|
||||
# UA[${requestHeader.user-agent}] Params[${requestParameters}]
|
||||
# ResponseEncoding[${responseEncodingNoDefault}]
|
||||
# log_exceptions: true
|
||||
# name: fhirtest.access
|
||||
# max_binary_size: 104857600
|
||||
# max_page_size: 200
|
||||
# retain_cached_searches_mins: 60
|
||||
# reuse_cached_search_results_millis: 60000
|
||||
tester:
|
||||
home:
|
||||
name: FHIRFLARE Tester
|
||||
server_address: http://localhost:8080/fhir
|
||||
refuse_to_fetch_third_party_urls: false
|
||||
fhir_version: R4
|
||||
global:
|
||||
name: Global Tester
|
||||
server_address: "http://hapi.fhir.org/baseR4"
|
||||
refuse_to_fetch_third_party_urls: false
|
||||
fhir_version: R4
|
||||
# validation:
|
||||
# requests_enabled: true
|
||||
# responses_enabled: true
|
||||
# binary_storage_enabled: true
|
||||
inline_resource_storage_below_size: 4000
|
||||
# bulk_export_enabled: true
|
||||
# subscription:
|
||||
# resthook_enabled: true
|
||||
# websocket_enabled: false
|
||||
# polling_interval_ms: 5000
|
||||
# immediately_queued: false
|
||||
# email:
|
||||
# from: some@test.com
|
||||
# host: google.com
|
||||
# port:
|
||||
# username:
|
||||
# password:
|
||||
# auth:
|
||||
# startTlsEnable:
|
||||
# startTlsRequired:
|
||||
# quitWait:
|
||||
# lastn_enabled: true
|
||||
# store_resource_in_lucene_index_enabled: true
|
||||
### This is configuration for normalized quantity search level default is 0
|
||||
### 0: NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED - default
|
||||
### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED
|
||||
### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED
|
||||
# normalized_quantity_search_level: 2
|
||||
#elasticsearch:
|
||||
# debug:
|
||||
# pretty_print_json_log: false
|
||||
# refresh_after_write: false
|
||||
# enabled: false
|
||||
# password: SomePassword
|
||||
# required_index_status: YELLOW
|
||||
# rest_url: 'localhost:9200'
|
||||
# protocol: 'http'
|
||||
# schema_management_strategy: CREATE
|
||||
# username: SomeUsername
|
1
hapi-fhir-jpaserver/target/ROOT.war
Normal file
1
hapi-fhir-jpaserver/target/ROOT.war
Normal file
@ -0,0 +1 @@
|
||||
|
1
hapi-fhir-jpaserver/target/classes/application.yaml
Normal file
1
hapi-fhir-jpaserver/target/classes/application.yaml
Normal file
@ -0,0 +1 @@
|
||||
|
Binary file not shown.
@ -0,0 +1,22 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.au.base",
|
||||
"version": "5.1.0-preview",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hl7.terminology.r4",
|
||||
"version": "6.2.0"
|
||||
},
|
||||
{
|
||||
"name": "hl7.fhir.uv.extensions.r4",
|
||||
"version": "5.2.0"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-17T04:04:45.070781+00:00"
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.au.core",
|
||||
"version": "1.1.0-preview",
|
||||
"dependency_mode": "tree-shaking",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
@ -30,5 +30,5 @@
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-15T00:27:41.653977+00:00"
|
||||
"timestamp": "2025-04-17T04:04:20.523471+00:00"
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1",
|
||||
"dependency_mode": "tree-shaking",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-15T00:27:55.537600+00:00"
|
||||
"timestamp": "2025-04-17T04:04:29.230227+00:00"
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.extensions.r4",
|
||||
"version": "5.2.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-17T04:04:41.588025+00:00"
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.extensions.r4-5.2.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.extensions.r4-5.2.0.tgz
Normal file
Binary file not shown.
22
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
22
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.ipa",
|
||||
"version": "1.0.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hl7.terminology.r4",
|
||||
"version": "5.0.0"
|
||||
},
|
||||
{
|
||||
"name": "hl7.fhir.uv.smart-app-launch",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-17T04:04:49.395594+00:00"
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,14 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.smart-app-launch",
|
||||
"version": "2.0.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-17T04:04:56.492512+00:00"
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,18 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.smart-app-launch",
|
||||
"version": "2.1.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hl7.terminology.r4",
|
||||
"version": "5.0.0"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-17T04:04:46.943079+00:00"
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.1.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.1.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,14 @@
|
||||
{
|
||||
"package_name": "hl7.terminology.r4",
|
||||
"version": "5.0.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-17T04:04:54.857273+00:00"
|
||||
}
|
BIN
instance/fhir_packages/hl7.terminology.r4-5.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.terminology.r4-5.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,14 @@
|
||||
{
|
||||
"package_name": "hl7.terminology.r4",
|
||||
"version": "6.2.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": [],
|
||||
"timestamp": "2025-04-17T04:04:37.703082+00:00"
|
||||
}
|
BIN
instance/fhir_packages/hl7.terminology.r4-6.2.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.terminology.r4-6.2.0.tgz
Normal file
Binary file not shown.
6
instance/hapi-h2-data/fhir.lock.db
Normal file
6
instance/hapi-h2-data/fhir.lock.db
Normal file
@ -0,0 +1,6 @@
|
||||
#FileLock
|
||||
#Mon Apr 21 13:28:52 UTC 2025
|
||||
server=172.18.0.2\:37033
|
||||
hostName=d3691f0dbfcf
|
||||
method=file
|
||||
id=196588987143154de87600de82a07a5280026b49718
|
BIN
instance/hapi-h2-data/fhir.mv.db
Normal file
BIN
instance/hapi-h2-data/fhir.mv.db
Normal file
Binary file not shown.
63659
instance/hapi-h2-data/fhir.trace.db
Normal file
63659
instance/hapi-h2-data/fhir.trace.db
Normal file
File diff suppressed because it is too large
Load Diff
8
logs/flask.log
Normal file
8
logs/flask.log
Normal file
@ -0,0 +1,8 @@
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
311
logs/flask_err.log
Normal file
311
logs/flask_err.log
Normal file
@ -0,0 +1,311 @@
|
||||
INFO:__main__:Application running in mode: lite
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
INFO:werkzeug:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://172.18.0.2:5000
|
||||
INFO:werkzeug:[33mPress CTRL+C to quit[0m
|
||||
INFO:__main__:Application running in mode: lite
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
INFO:werkzeug:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://172.18.0.2:5000
|
||||
INFO:werkzeug:[33mPress CTRL+C to quit[0m
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:15] "GET / HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:15] "GET /static/FHIRFLARE.png HTTP/1.1" 200 -
|
||||
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
|
||||
DEBUG:services:Parsed 'hl7.fhir.au.base-5.1.0-preview.tgz' -> name='hl7.fhir.au.base-5.1.0', version='preview'
|
||||
DEBUG:services:Parsed 'hl7.fhir.au.core-1.1.0-preview.tgz' -> name='hl7.fhir.au.core-1.1.0', version='preview'
|
||||
DEBUG:services:Parsed 'hl7.fhir.r4.core-4.0.1.tgz' -> name='hl7.fhir.r4.core', version='4.0.1'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.extensions.r4-5.2.0.tgz' -> name='hl7.fhir.uv.extensions.r4', version='5.2.0'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.ipa-1.0.0.tgz' -> name='hl7.fhir.uv.ipa', version='1.0.0'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.0.0'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.1.0'
|
||||
DEBUG:services:Parsed 'hl7.terminology.r4-5.0.0.tgz' -> name='hl7.terminology.r4', version='5.0.0'
|
||||
DEBUG:services:Parsed 'hl7.terminology.r4-6.2.0.tgz' -> name='hl7.terminology.r4', version='6.2.0'
|
||||
DEBUG:__main__:Found packages: [{'name': 'hl7.fhir.au.base', 'version': '5.1.0-preview', 'filename': 'hl7.fhir.au.base-5.1.0-preview.tgz'}, {'name': 'hl7.fhir.au.core', 'version': '1.1.0-preview', 'filename': 'hl7.fhir.au.core-1.1.0-preview.tgz'}, {'name': 'hl7.fhir.r4.core', 'version': '4.0.1', 'filename': 'hl7.fhir.r4.core-4.0.1.tgz'}, {'name': 'hl7.fhir.uv.extensions.r4', 'version': '5.2.0', 'filename': 'hl7.fhir.uv.extensions.r4-5.2.0.tgz'}, {'name': 'hl7.fhir.uv.ipa', 'version': '1.0.0', 'filename': 'hl7.fhir.uv.ipa-1.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.0.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.1.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '5.0.0', 'filename': 'hl7.terminology.r4-5.0.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '6.2.0', 'filename': 'hl7.terminology.r4-6.2.0.tgz'}]
|
||||
DEBUG:__main__:Errors during package listing: []
|
||||
DEBUG:__main__:Duplicate groups: {'hl7.fhir.uv.smart-app-launch': ['2.0.0', '2.1.0'], 'hl7.terminology.r4': ['5.0.0', '6.2.0']}
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:33] "GET /view-igs HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:33] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
DEBUG:__main__:Viewing IG hl7.fhir.au.core-1.1.0#preview: 25 profiles, 17 base resources, 1 optional elements
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:35] "GET /view-ig/1 HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:35] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
DEBUG:__main__:Attempting to find SD for 'au-core-patient' in hl7.fhir.au.core-1.1.0-preview.tgz
|
||||
DEBUG:services:Searching for SD matching 'au-core-patient' with profile 'None' in hl7.fhir.au.core-1.1.0-preview.tgz
|
||||
DEBUG:services:Match found based on exact sd_id: au-core-patient
|
||||
DEBUG:services:Removing narrative text from resource: StructureDefinition
|
||||
INFO:services:Found high-confidence match for 'au-core-patient' (package/StructureDefinition-au-core-patient.json), stopping search.
|
||||
DEBUG:__main__:Found 20 unique IDs in differential.
|
||||
DEBUG:__main__:Processing 78 snapshot elements to add isInDifferential flag.
|
||||
DEBUG:__main__:Retrieved 19 Must Support paths for 'au-core-patient' from processed IG DB record.
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:36:42] "GET /get-structure?package_name=hl7.fhir.au.core-1.1.0&package_version=preview&resource_type=au-core-patient&view=snapshot HTTP/1.1" 200 -
|
||||
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Found 9 .tgz files: ['hl7.fhir.au.base-5.1.0-preview.tgz', 'hl7.fhir.au.core-1.1.0-preview.tgz', 'hl7.fhir.r4.core-4.0.1.tgz', 'hl7.fhir.uv.extensions.r4-5.2.0.tgz', 'hl7.fhir.uv.ipa-1.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz', 'hl7.terminology.r4-5.0.0.tgz', 'hl7.terminology.r4-6.2.0.tgz']
|
||||
DEBUG:__main__:Added package: hl7.fhir.au.base#5.1.0-preview
|
||||
DEBUG:__main__:Added package: hl7.fhir.au.core#1.1.0-preview
|
||||
DEBUG:__main__:Added package: hl7.fhir.r4.core#4.0.1
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.extensions.r4#5.2.0
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.ipa#1.0.0
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.0.0
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.1.0
|
||||
DEBUG:__main__:Added package: hl7.terminology.r4#5.0.0
|
||||
DEBUG:__main__:Added package: hl7.terminology.r4#6.2.0
|
||||
DEBUG:__main__:Set package choices: [('', 'None'), ('hl7.fhir.au.base#5.1.0-preview', 'hl7.fhir.au.base#5.1.0-preview'), ('hl7.fhir.au.core#1.1.0-preview', 'hl7.fhir.au.core#1.1.0-preview'), ('hl7.fhir.r4.core#4.0.1', 'hl7.fhir.r4.core#4.0.1'), ('hl7.fhir.uv.extensions.r4#5.2.0', 'hl7.fhir.uv.extensions.r4#5.2.0'), ('hl7.fhir.uv.ipa#1.0.0', 'hl7.fhir.uv.ipa#1.0.0'), ('hl7.fhir.uv.smart-app-launch#2.0.0', 'hl7.fhir.uv.smart-app-launch#2.0.0'), ('hl7.fhir.uv.smart-app-launch#2.1.0', 'hl7.fhir.uv.smart-app-launch#2.1.0'), ('hl7.terminology.r4#5.0.0', 'hl7.terminology.r4#5.0.0'), ('hl7.terminology.r4#6.2.0', 'hl7.terminology.r4#6.2.0')]
|
||||
DEBUG:__main__:Handling GET request for FSH converter page.
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "GET /fsh-converter HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "[36mGET /static/js/lottie.min.js HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:37:59] "[36mGET /static/animations/loading-light.json HTTP/1.1[0m" 304 -
|
||||
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Found 9 .tgz files: ['hl7.fhir.au.base-5.1.0-preview.tgz', 'hl7.fhir.au.core-1.1.0-preview.tgz', 'hl7.fhir.r4.core-4.0.1.tgz', 'hl7.fhir.uv.extensions.r4-5.2.0.tgz', 'hl7.fhir.uv.ipa-1.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz', 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz', 'hl7.terminology.r4-5.0.0.tgz', 'hl7.terminology.r4-6.2.0.tgz']
|
||||
DEBUG:__main__:Added package: hl7.fhir.au.base#5.1.0-preview
|
||||
DEBUG:__main__:Added package: hl7.fhir.au.core#1.1.0-preview
|
||||
DEBUG:__main__:Added package: hl7.fhir.r4.core#4.0.1
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.extensions.r4#5.2.0
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.ipa#1.0.0
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.0.0
|
||||
DEBUG:__main__:Added package: hl7.fhir.uv.smart-app-launch#2.1.0
|
||||
DEBUG:__main__:Added package: hl7.terminology.r4#5.0.0
|
||||
DEBUG:__main__:Added package: hl7.terminology.r4#6.2.0
|
||||
DEBUG:__main__:Set package choices: [('', 'None'), ('hl7.fhir.au.base#5.1.0-preview', 'hl7.fhir.au.base#5.1.0-preview'), ('hl7.fhir.au.core#1.1.0-preview', 'hl7.fhir.au.core#1.1.0-preview'), ('hl7.fhir.r4.core#4.0.1', 'hl7.fhir.r4.core#4.0.1'), ('hl7.fhir.uv.extensions.r4#5.2.0', 'hl7.fhir.uv.extensions.r4#5.2.0'), ('hl7.fhir.uv.ipa#1.0.0', 'hl7.fhir.uv.ipa#1.0.0'), ('hl7.fhir.uv.smart-app-launch#2.0.0', 'hl7.fhir.uv.smart-app-launch#2.0.0'), ('hl7.fhir.uv.smart-app-launch#2.1.0', 'hl7.fhir.uv.smart-app-launch#2.1.0'), ('hl7.terminology.r4#5.0.0', 'hl7.terminology.r4#5.0.0'), ('hl7.terminology.r4#6.2.0', 'hl7.terminology.r4#6.2.0')]
|
||||
DEBUG:__main__:Processing input: mode=text, has_file=False, has_text=True, has_alias=False
|
||||
DEBUG:services:Processed input: ('/tmp/tmpkn9pyg0k/input.json', None)
|
||||
DEBUG:__main__:Running GoFSH with input: /tmp/tmpkn9pyg0k/input.json, output_dir: /app/static/uploads/fsh_output
|
||||
DEBUG:services:Wrapper script contents:
|
||||
#!/bin/bash
|
||||
exec 3>/dev/null
|
||||
"gofsh" "/tmp/tmpkn9pyg0k/input.json" "-o" "/tmp/tmp37ihga7h" "-s" "file-per-definition" "-l" "error" "-u" "4.3.0" "--dependency" "hl7.fhir.us.core@6.1.0" "--indent" </dev/null >/tmp/gofsh_output.log 2>&1
|
||||
|
||||
DEBUG:services:Temp output directory contents before GoFSH: []
|
||||
DEBUG:services:GoFSH output:
|
||||
|
||||
╔═════════════════════════ GoFSH RESULTS ═════════════════════════╗
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Profiles │ Extensions │ Logicals │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 0 │ 0 │ 0 │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Resources │ ValueSets │ CodeSystems │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 0 │ 0 │ 0 │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Instances │ Invariants │ Mappings │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 1 │ 0 │ 0 │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Aliases │ │ │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 1 │ │ │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ║
|
||||
╠═════════════════════════════════════════════════════════════════╣
|
||||
║ Not bad, but it cod be batter! 0 Errors 1 Warning ║
|
||||
╚═════════════════════════════════════════════════════════════════╝
|
||||
|
||||
DEBUG:services:GoFSH fishing-trip wrapper script contents:
|
||||
#!/bin/bash
|
||||
exec 3>/dev/null
|
||||
exec >/dev/null 2>&1
|
||||
"gofsh" "/tmp/tmpkn9pyg0k/input.json" "-o" "/tmp/tmpb_q8uf73" "-s" "file-per-definition" "-l" "error" "--fshing-trip" "-u" "4.3.0" "--dependency" "hl7.fhir.us.core@6.1.0" "--indent" </dev/null >/tmp/gofsh_output.log 2>&1
|
||||
|
||||
DEBUG:services:GoFSH fishing-trip output:
|
||||
|
||||
╔═════════════════════════ GoFSH RESULTS ═════════════════════════╗
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Profiles │ Extensions │ Logicals │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 0 │ 0 │ 0 │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Resources │ ValueSets │ CodeSystems │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 0 │ 0 │ 0 │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Instances │ Invariants │ Mappings │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 1 │ 0 │ 0 │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
|
||||
║ │ Aliases │ │ │ ║
|
||||
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
|
||||
║ │ 1 │ │ │ ║
|
||||
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
|
||||
║ ║
|
||||
╠═════════════════════════════════════════════════════════════════╣
|
||||
║ Warnings... Water those about? 0 Errors 1 Warning ║
|
||||
╚═════════════════════════════════════════════════════════════════╝
|
||||
╔═════════════════════════════════════════════════════════════════╗
|
||||
║ Generating round trip results via SUSHI ║
|
||||
╚═════════════════════════════════════════════════════════════════╝
|
||||
info Running SUSHI v3.15.0 (implements FHIR Shorthand specification v3.0.0)
|
||||
info Arguments:
|
||||
info /tmp/tmpb_q8uf73
|
||||
info No output path specified. Output to /tmp/tmpb_q8uf73
|
||||
info Using configuration file: /tmp/tmpb_q8uf73/sushi-config.yaml
|
||||
warn The FSHOnly property is set to true, so no output specific to IG creation will be generated. The following properties are unused and only relevant for IG creation: id, name. Consider removing these properties from sushi-config.yaml.
|
||||
File: /tmp/tmpb_q8uf73/sushi-config.yaml
|
||||
info Importing FSH text...
|
||||
info Preprocessed 2 documents with 1 aliases.
|
||||
info Imported 0 definitions and 1 instances.
|
||||
info Loaded virtual package sushi-r5forR4#1.0.0 with 7 resources
|
||||
info Resolved hl7.fhir.uv.tools.r4#latest to concrete version 0.5.0
|
||||
info Loaded hl7.fhir.uv.tools.r4#0.5.0 with 88 resources
|
||||
info Resolved hl7.terminology.r4#latest to concrete version 6.2.0
|
||||
info Loaded hl7.terminology.r4#6.2.0 with 4323 resources
|
||||
info Resolved hl7.fhir.uv.extensions.r4#latest to concrete version 5.2.0
|
||||
info Loaded hl7.fhir.uv.extensions.r4#5.2.0 with 759 resources
|
||||
info Loaded hl7.fhir.us.core#6.1.0 with 210 resources
|
||||
info Loaded hl7.fhir.r4b.core#4.3.0 with 3497 resources
|
||||
info Loaded virtual package sushi-local#LOCAL with 0 resources
|
||||
info Converting FSH to FHIR resources...
|
||||
info Converted 1 FHIR instances.
|
||||
info Exporting FHIR resources as JSON...
|
||||
info Exported 1 FHIR resources as JSON.
|
||||
info Exporting FSH definitions only. No IG related content will be exported.
|
||||
|
||||
========================= SUSHI RESULTS ===========================
|
||||
| ------------------------------------------------------------- |
|
||||
| | Profiles | Extensions | Logicals | Resources | |
|
||||
| |-------------------------------------------------------------| |
|
||||
| | 0 | 0 | 0 | 0 | |
|
||||
| ------------------------------------------------------------- |
|
||||
| ------------------------------------------------------------- |
|
||||
| | ValueSets | CodeSystems | Instances | |
|
||||
| |-------------------------------------------------------------| |
|
||||
| | 0 | 0 | 1 | |
|
||||
| ------------------------------------------------------------- |
|
||||
| |
|
||||
===================================================================
|
||||
| You might need some Vitamin Sea 0 Errors 1 Warning |
|
||||
===================================================================
|
||||
|
||||
|
||||
DEBUG:services:Copied files to final output directory: ['sushi-config.yaml', 'input/fsh/aliases.fsh', 'input/fsh/instances/discharge-1.fsh', 'input/input.json', 'fshing-trip-comparison.html']
|
||||
INFO:services:GoFSH executed successfully for /tmp/tmpkn9pyg0k/input.json
|
||||
DEBUG:__main__:Successfully removed temp directory: /tmp/tmpkn9pyg0k
|
||||
INFO:__main__:FSH conversion successful
|
||||
DEBUG:__main__:Returning partial HTML for AJAX POST request.
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:26] "POST /fsh-converter HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:26] "[36mGET /static/animations/loading-light.json HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:42] "GET /static/uploads/fsh_output/fshing-trip-comparison.html HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:40:59] "GET /fhir-ui-operations HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:41:00] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:27] "GET /fhir HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:27] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:31] "GET /fhir-ui-operations HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:31] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:39] "GET /fhir HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:39] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:48] "GET /validate-sample HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:43:48] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:44:08] "GET / HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:44:08] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
DEBUG:__main__:Scanning packages directory: /app/instance/fhir_packages
|
||||
DEBUG:services:Parsed 'hl7.fhir.au.base-5.1.0-preview.tgz' -> name='hl7.fhir.au.base-5.1.0', version='preview'
|
||||
DEBUG:services:Parsed 'hl7.fhir.au.core-1.1.0-preview.tgz' -> name='hl7.fhir.au.core-1.1.0', version='preview'
|
||||
DEBUG:services:Parsed 'hl7.fhir.r4.core-4.0.1.tgz' -> name='hl7.fhir.r4.core', version='4.0.1'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.extensions.r4-5.2.0.tgz' -> name='hl7.fhir.uv.extensions.r4', version='5.2.0'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.ipa-1.0.0.tgz' -> name='hl7.fhir.uv.ipa', version='1.0.0'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.0.0'
|
||||
DEBUG:services:Parsed 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz' -> name='hl7.fhir.uv.smart-app-launch', version='2.1.0'
|
||||
DEBUG:services:Parsed 'hl7.terminology.r4-5.0.0.tgz' -> name='hl7.terminology.r4', version='5.0.0'
|
||||
DEBUG:services:Parsed 'hl7.terminology.r4-6.2.0.tgz' -> name='hl7.terminology.r4', version='6.2.0'
|
||||
DEBUG:__main__:Found packages: [{'name': 'hl7.fhir.au.base', 'version': '5.1.0-preview', 'filename': 'hl7.fhir.au.base-5.1.0-preview.tgz'}, {'name': 'hl7.fhir.au.core', 'version': '1.1.0-preview', 'filename': 'hl7.fhir.au.core-1.1.0-preview.tgz'}, {'name': 'hl7.fhir.r4.core', 'version': '4.0.1', 'filename': 'hl7.fhir.r4.core-4.0.1.tgz'}, {'name': 'hl7.fhir.uv.extensions.r4', 'version': '5.2.0', 'filename': 'hl7.fhir.uv.extensions.r4-5.2.0.tgz'}, {'name': 'hl7.fhir.uv.ipa', 'version': '1.0.0', 'filename': 'hl7.fhir.uv.ipa-1.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.0.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.0.0.tgz'}, {'name': 'hl7.fhir.uv.smart-app-launch', 'version': '2.1.0', 'filename': 'hl7.fhir.uv.smart-app-launch-2.1.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '5.0.0', 'filename': 'hl7.terminology.r4-5.0.0.tgz'}, {'name': 'hl7.terminology.r4', 'version': '6.2.0', 'filename': 'hl7.terminology.r4-6.2.0.tgz'}]
|
||||
DEBUG:__main__:Errors during package listing: []
|
||||
DEBUG:__main__:Duplicate groups: {'hl7.fhir.uv.smart-app-launch': ['2.0.0', '2.1.0'], 'hl7.terminology.r4': ['5.0.0', '6.2.0']}
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:07] "GET /view-igs HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:07] "GET /static/FHIRFLARE.png HTTP/1.1" 200 -
|
||||
DEBUG:__main__:Viewing IG hl7.fhir.au.core-1.1.0#preview: 25 profiles, 17 base resources, 1 optional elements
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:13] "GET /view-ig/1 HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:13] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
DEBUG:__main__:Attempting to find SD for 'au-core-patient' in hl7.fhir.au.core-1.1.0-preview.tgz
|
||||
DEBUG:services:Searching for SD matching 'au-core-patient' with profile 'None' in hl7.fhir.au.core-1.1.0-preview.tgz
|
||||
DEBUG:services:Match found based on exact sd_id: au-core-patient
|
||||
DEBUG:services:Removing narrative text from resource: StructureDefinition
|
||||
INFO:services:Found high-confidence match for 'au-core-patient' (package/StructureDefinition-au-core-patient.json), stopping search.
|
||||
DEBUG:__main__:Found 20 unique IDs in differential.
|
||||
DEBUG:__main__:Processing 78 snapshot elements to add isInDifferential flag.
|
||||
DEBUG:__main__:Retrieved 19 Must Support paths for 'au-core-patient' from processed IG DB record.
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:17] "GET /get-structure?package_name=hl7.fhir.au.core-1.1.0&package_version=preview&resource_type=au-core-patient&view=snapshot HTTP/1.1" 200 -
|
||||
DEBUG:__main__:Removing narrative text from example 'package/example/Patient-banks-mia-leanne.json'
|
||||
INFO:werkzeug:172.18.0.1 - - [21/Apr/2025 23:45:32] "GET /get-example?package_name=hl7.fhir.au.core-1.1.0&package_version=preview&filename=package/example/Patient-banks-mia-leanne.json HTTP/1.1" 200 -
|
||||
INFO:__main__:Application running in mode: lite
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
INFO:werkzeug:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://172.18.0.2:5000
|
||||
INFO:werkzeug:[33mPress CTRL+C to quit[0m
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:38] "GET / HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:38] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:44] "GET /about HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:12:44] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:13:32] "GET / HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:13:32] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
||||
INFO:__main__:Application running in mode: lite
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
DEBUG:__main__:Instance path configuration: /app/instance
|
||||
DEBUG:__main__:Database URI: sqlite:////app/instance/fhir_ig.db
|
||||
DEBUG:__main__:Packages path: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Flask instance folder path: /app/instance
|
||||
DEBUG:__main__:Directories created/verified: Instance: /app/instance, Packages: /app/instance/fhir_packages
|
||||
DEBUG:__main__:Attempting to create database tables for URI: sqlite:////app/instance/fhir_ig.db
|
||||
INFO:__main__:Database tables created successfully (if they didn't exist).
|
||||
INFO:werkzeug:[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:5000
|
||||
* Running on http://172.18.0.2:5000
|
||||
INFO:werkzeug:[33mPress CTRL+C to quit[0m
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:28:51] "GET / HTTP/1.1" 200 -
|
||||
INFO:werkzeug:172.18.0.1 - - [22/Apr/2025 00:28:52] "[36mGET /static/FHIRFLARE.png HTTP/1.1[0m" 304 -
|
40
logs/supervisord.log
Normal file
40
logs/supervisord.log
Normal file
@ -0,0 +1,40 @@
|
||||
2025-04-21 23:28:14,353 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
|
||||
2025-04-21 23:28:14,360 INFO supervisord started with pid 1
|
||||
2025-04-21 23:28:15,369 INFO spawned: 'flask' with pid 8
|
||||
2025-04-21 23:28:15,377 INFO spawned: 'tomcat' with pid 9
|
||||
2025-04-21 23:28:26,240 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
|
||||
2025-04-21 23:28:46,263 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
|
||||
2025-04-21 23:32:40,570 WARN received SIGTERM indicating exit request
|
||||
2025-04-21 23:32:40,603 INFO waiting for flask, tomcat to die
|
||||
2025-04-21 23:32:42,242 WARN stopped: tomcat (exit status 143)
|
||||
2025-04-21 23:32:43,301 WARN stopped: flask (terminated by SIGTERM)
|
||||
2025-04-21 23:36:02,369 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
|
||||
2025-04-21 23:36:02,377 INFO supervisord started with pid 1
|
||||
2025-04-21 23:36:03,393 INFO spawned: 'flask' with pid 7
|
||||
2025-04-21 23:36:03,407 INFO spawned: 'tomcat' with pid 8
|
||||
2025-04-21 23:36:13,571 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
|
||||
2025-04-21 23:36:34,045 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
|
||||
2025-04-21 23:51:48,038 WARN received SIGTERM indicating exit request
|
||||
2025-04-21 23:51:48,042 INFO waiting for flask, tomcat to die
|
||||
2025-04-21 23:51:50,438 WARN stopped: tomcat (exit status 143)
|
||||
2025-04-21 23:51:51,472 WARN stopped: flask (terminated by SIGTERM)
|
||||
2025-04-22 00:09:45,824 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
|
||||
2025-04-22 00:09:45,840 INFO supervisord started with pid 1
|
||||
2025-04-22 00:09:46,858 INFO spawned: 'flask' with pid 7
|
||||
2025-04-22 00:09:46,862 INFO spawned: 'tomcat' with pid 8
|
||||
2025-04-22 00:09:57,177 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
|
||||
2025-04-22 00:10:17,198 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
|
||||
2025-04-22 00:25:04,477 WARN received SIGTERM indicating exit request
|
||||
2025-04-22 00:25:04,493 INFO waiting for flask, tomcat to die
|
||||
2025-04-22 00:25:05,830 WARN stopped: tomcat (exit status 143)
|
||||
2025-04-22 00:25:06,852 WARN stopped: flask (terminated by SIGTERM)
|
||||
2025-04-22 00:27:24,713 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message.
|
||||
2025-04-22 00:27:24,719 INFO supervisord started with pid 1
|
||||
2025-04-22 00:27:25,731 INFO spawned: 'flask' with pid 7
|
||||
2025-04-22 00:27:25,736 INFO spawned: 'tomcat' with pid 8
|
||||
2025-04-22 00:27:36,540 INFO success: flask entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)
|
||||
2025-04-22 00:27:56,565 INFO success: tomcat entered RUNNING state, process has stayed up for > than 30 seconds (startsecs)
|
||||
2025-04-22 00:54:33,995 WARN received SIGTERM indicating exit request
|
||||
2025-04-22 00:54:34,047 INFO waiting for flask, tomcat to die
|
||||
2025-04-22 00:54:34,740 WARN stopped: tomcat (exit status 143)
|
||||
2025-04-22 00:54:35,770 WARN stopped: flask (terminated by SIGTERM)
|
1
logs/supervisord.pid
Normal file
1
logs/supervisord.pid
Normal file
@ -0,0 +1 @@
|
||||
1
|
0
logs/tomcat.log
Normal file
0
logs/tomcat.log
Normal file
680
logs/tomcat_err.log
Normal file
680
logs/tomcat_err.log
Normal file
@ -0,0 +1,680 @@
|
||||
21-Apr-2025 23:28:16.155 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
|
||||
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
|
||||
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
|
||||
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
|
||||
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
|
||||
21-Apr-2025 23:28:16.164 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
|
||||
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
|
||||
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
|
||||
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
|
||||
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
|
||||
21-Apr-2025 23:28:16.165 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
|
||||
21-Apr-2025 23:28:16.187 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
|
||||
21-Apr-2025 23:28:16.188 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
|
||||
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
|
||||
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
|
||||
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
|
||||
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
|
||||
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
|
||||
21-Apr-2025 23:28:16.189 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
|
||||
21-Apr-2025 23:28:16.196 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
|
||||
21-Apr-2025 23:28:16.200 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
|
||||
21-Apr-2025 23:28:16.564 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:28:16.604 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [733] milliseconds
|
||||
21-Apr-2025 23:28:16.680 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
|
||||
21-Apr-2025 23:28:16.680 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
|
||||
21-Apr-2025 23:28:16.703 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
21-Apr-2025 23:28:16.727 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
|
||||
java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
|
||||
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
|
||||
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
|
||||
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
|
||||
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
|
||||
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
21-Apr-2025 23:28:16.769 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
java.lang.IllegalStateException: Error starting child
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@5a5a729f]
|
||||
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
|
||||
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
|
||||
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
... 37 more
|
||||
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
|
||||
... 44 more
|
||||
Caused by: java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
|
||||
... 45 more
|
||||
21-Apr-2025 23:28:16.773 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [68] ms
|
||||
21-Apr-2025 23:28:16.774 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
|
||||
21-Apr-2025 23:28:17.183 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [409] ms
|
||||
21-Apr-2025 23:28:17.184 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
|
||||
21-Apr-2025 23:28:17.203 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [19] ms
|
||||
21-Apr-2025 23:28:17.208 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:28:17.227 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [623] milliseconds
|
||||
21-Apr-2025 23:32:40.652 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:32:40.760 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
|
||||
21-Apr-2025 23:32:40.883 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:32:40.910 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:36:04.469 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
|
||||
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
|
||||
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
|
||||
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
|
||||
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
|
||||
21-Apr-2025 23:36:04.476 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
|
||||
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
|
||||
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
|
||||
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
|
||||
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
|
||||
21-Apr-2025 23:36:04.477 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
|
||||
21-Apr-2025 23:36:04.503 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
|
||||
21-Apr-2025 23:36:04.503 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
|
||||
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
|
||||
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
|
||||
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
|
||||
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
|
||||
21-Apr-2025 23:36:04.504 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
|
||||
21-Apr-2025 23:36:04.505 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
|
||||
21-Apr-2025 23:36:04.505 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
|
||||
21-Apr-2025 23:36:04.505 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
|
||||
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
|
||||
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
|
||||
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
|
||||
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
|
||||
21-Apr-2025 23:36:04.506 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
|
||||
21-Apr-2025 23:36:04.519 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
|
||||
21-Apr-2025 23:36:04.524 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
|
||||
21-Apr-2025 23:36:05.240 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:36:05.313 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [1239] milliseconds
|
||||
21-Apr-2025 23:36:05.489 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
|
||||
21-Apr-2025 23:36:05.490 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
|
||||
21-Apr-2025 23:36:05.551 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
21-Apr-2025 23:36:05.590 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
|
||||
java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
|
||||
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
|
||||
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
|
||||
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
|
||||
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
|
||||
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
21-Apr-2025 23:36:05.657 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
java.lang.IllegalStateException: Error starting child
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@d554c5f]
|
||||
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
|
||||
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
|
||||
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
... 37 more
|
||||
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
|
||||
... 44 more
|
||||
Caused by: java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
|
||||
... 45 more
|
||||
21-Apr-2025 23:36:05.660 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [108] ms
|
||||
21-Apr-2025 23:36:05.662 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
|
||||
21-Apr-2025 23:36:06.431 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [769] ms
|
||||
21-Apr-2025 23:36:06.432 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
|
||||
21-Apr-2025 23:36:06.468 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [36] ms
|
||||
21-Apr-2025 23:36:06.478 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:36:06.561 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [1247] milliseconds
|
||||
21-Apr-2025 23:51:48.137 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:51:48.297 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
|
||||
21-Apr-2025 23:51:48.716 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
|
||||
21-Apr-2025 23:51:48.787 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:09:47.747 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
|
||||
22-Apr-2025 00:09:47.763 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
|
||||
22-Apr-2025 00:09:47.763 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
|
||||
22-Apr-2025 00:09:47.763 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
|
||||
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
|
||||
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
|
||||
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
|
||||
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
|
||||
22-Apr-2025 00:09:47.764 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
|
||||
22-Apr-2025 00:09:47.765 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
|
||||
22-Apr-2025 00:09:47.765 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
|
||||
22-Apr-2025 00:09:47.796 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
|
||||
22-Apr-2025 00:09:47.796 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
|
||||
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
|
||||
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
|
||||
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
|
||||
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
|
||||
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
|
||||
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
|
||||
22-Apr-2025 00:09:47.797 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
|
||||
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
|
||||
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
|
||||
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
|
||||
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
|
||||
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
|
||||
22-Apr-2025 00:09:47.798 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
|
||||
22-Apr-2025 00:09:47.815 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
|
||||
22-Apr-2025 00:09:47.822 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
|
||||
22-Apr-2025 00:09:48.310 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:09:48.348 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [997] milliseconds
|
||||
22-Apr-2025 00:09:48.467 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
|
||||
22-Apr-2025 00:09:48.467 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
|
||||
22-Apr-2025 00:09:48.489 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
22-Apr-2025 00:09:48.519 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
|
||||
java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
|
||||
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
|
||||
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
|
||||
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
|
||||
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
|
||||
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
22-Apr-2025 00:09:48.556 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
java.lang.IllegalStateException: Error starting child
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@68c9d179]
|
||||
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
|
||||
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
|
||||
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
... 37 more
|
||||
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
|
||||
... 44 more
|
||||
Caused by: java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
|
||||
... 45 more
|
||||
22-Apr-2025 00:09:48.560 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [69] ms
|
||||
22-Apr-2025 00:09:48.561 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
|
||||
22-Apr-2025 00:09:49.063 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [502] ms
|
||||
22-Apr-2025 00:09:49.063 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
|
||||
22-Apr-2025 00:09:49.084 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [21] ms
|
||||
22-Apr-2025 00:09:49.103 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:09:49.166 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [817] milliseconds
|
||||
22-Apr-2025 00:25:04.510 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:25:04.547 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
|
||||
22-Apr-2025 00:25:04.657 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:25:04.685 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:27:26.457 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/10.1.40
|
||||
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built: Apr 1 2025 17:20:53 UTC
|
||||
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 10.1.40.0
|
||||
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name: Linux
|
||||
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version: 5.15.167.4-microsoft-standard-WSL2
|
||||
22-Apr-2025 00:27:26.467 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture: amd64
|
||||
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home: /opt/java/openjdk
|
||||
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 17.0.14+7
|
||||
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor: Eclipse Adoptium
|
||||
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE: /usr/local/tomcat
|
||||
22-Apr-2025 00:27:26.468 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME: /usr/local/tomcat
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djdk.tls.ephemeralDHKeySize=2048
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.protocol.handler.pkgs=org.apache.catalina.webresources
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dsun.io.useCanonCaches=false
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dorg.apache.catalina.security.SecurityListener.UMASK=0027
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang=ALL-UNNAMED
|
||||
22-Apr-2025 00:27:26.492 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
|
||||
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.io=ALL-UNNAMED
|
||||
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util=ALL-UNNAMED
|
||||
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.base/java.util.concurrent=ALL-UNNAMED
|
||||
22-Apr-2025 00:27:26.493 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
|
||||
22-Apr-2025 00:27:26.494 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.base=/usr/local/tomcat
|
||||
22-Apr-2025 00:27:26.494 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Dcatalina.home=/usr/local/tomcat
|
||||
22-Apr-2025 00:27:26.494 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Command line argument: -Djava.io.tmpdir=/usr/local/tomcat/temp
|
||||
22-Apr-2025 00:27:26.501 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent Loaded Apache Tomcat Native library [2.0.8] using APR version [1.7.2].
|
||||
22-Apr-2025 00:27:26.505 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL OpenSSL successfully initialized [OpenSSL 3.0.13 30 Jan 2024]
|
||||
22-Apr-2025 00:27:26.893 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:27:26.937 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [790] milliseconds
|
||||
22-Apr-2025 00:27:27.009 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
|
||||
22-Apr-2025 00:27:27.009 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/10.1.40]
|
||||
22-Apr-2025 00:27:27.033 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
22-Apr-2025 00:27:27.058 SEVERE [main] org.apache.catalina.startup.ContextConfig.beforeStart Exception fixing docBase for context []
|
||||
java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.<init>(URLJarFile.java:103)
|
||||
at java.base/sun.net.www.protocol.jar.URLJarFile.getJarFile(URLJarFile.java:72)
|
||||
at java.base/sun.net.www.protocol.jar.JarFileFactory.get(JarFileFactory.java:168)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:131)
|
||||
at java.base/sun.net.www.protocol.jar.JarURLConnection.getJarFile(JarURLConnection.java:92)
|
||||
at org.apache.catalina.startup.ExpandWar.expand(ExpandWar.java:123)
|
||||
at org.apache.catalina.startup.ContextConfig.fixDocBase(ContextConfig.java:812)
|
||||
at org.apache.catalina.startup.ContextConfig.beforeStart(ContextConfig.java:948)
|
||||
at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:292)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:163)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
22-Apr-2025 00:27:27.096 SEVERE [main] org.apache.catalina.startup.HostConfig.deployWAR Error deploying web application archive [/usr/local/tomcat/webapps/ROOT.war]
|
||||
java.lang.IllegalStateException: Error starting child
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:602)
|
||||
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:571)
|
||||
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:654)
|
||||
at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:964)
|
||||
at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1902)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:123)
|
||||
at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:769)
|
||||
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:420)
|
||||
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1620)
|
||||
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:303)
|
||||
at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:109)
|
||||
at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:389)
|
||||
at org.apache.catalina.util.LifecycleBase.setState(LifecycleBase.java:336)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:776)
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203)
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193)
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749)
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:412)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:870)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.startup.Catalina.start(Catalina.java:761)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
|
||||
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)
|
||||
Caused by: org.apache.catalina.LifecycleException: Failed to initialize component [org.apache.catalina.webresources.WarResourceSet@4b520ea8]
|
||||
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:406)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:125)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:155)
|
||||
at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:723)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4159)
|
||||
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4281)
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164)
|
||||
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:599)
|
||||
... 37 more
|
||||
Caused by: java.lang.IllegalArgumentException: java.util.zip.ZipException: zip END header not found
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:141)
|
||||
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:122)
|
||||
... 44 more
|
||||
Caused by: java.util.zip.ZipException: zip END header not found
|
||||
at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1637)
|
||||
at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1645)
|
||||
at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1483)
|
||||
at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1445)
|
||||
at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:717)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:251)
|
||||
at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:180)
|
||||
at java.base/java.util.jar.JarFile.<init>(JarFile.java:346)
|
||||
at org.apache.catalina.webresources.AbstractSingleArchiveResourceSet.initInternal(AbstractSingleArchiveResourceSet.java:138)
|
||||
... 45 more
|
||||
22-Apr-2025 00:27:27.100 INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/usr/local/tomcat/webapps/ROOT.war] has finished in [67] ms
|
||||
22-Apr-2025 00:27:27.101 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/custom]
|
||||
22-Apr-2025 00:27:27.481 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/custom] has finished in [379] ms
|
||||
22-Apr-2025 00:27:27.482 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/app]
|
||||
22-Apr-2025 00:27:27.504 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/app] has finished in [22] ms
|
||||
22-Apr-2025 00:27:27.510 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:27:27.530 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [593] milliseconds
|
||||
22-Apr-2025 00:54:34.132 INFO [Thread-1] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:54:34.299 INFO [Thread-1] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
|
||||
22-Apr-2025 00:54:34.497 INFO [Thread-1] org.apache.coyote.AbstractProtocol.stop Stopping ProtocolHandler ["http-nio-8080"]
|
||||
22-Apr-2025 00:54:34.519 INFO [Thread-1] org.apache.coyote.AbstractProtocol.destroy Destroying ProtocolHandler ["http-nio-8080"]
|
607
services.py
607
services.py
@ -4,10 +4,16 @@ import tarfile
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
import shutil
|
||||
import sqlite3
|
||||
from flask import current_app, Blueprint, request, jsonify
|
||||
from fhirpathpy import evaluate
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
import datetime
|
||||
import subprocess
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Define Blueprint
|
||||
services_bp = Blueprint('services', __name__)
|
||||
@ -56,6 +62,7 @@ FHIR_R4_BASE_TYPES = {
|
||||
"SubstanceSourceMaterial", "SubstanceSpecification", "SupplyDelivery", "SupplyRequest", "Task",
|
||||
"TerminologyCapabilities", "TestReport", "TestScript", "ValueSet", "VerificationResult", "VisionPrescription"
|
||||
}
|
||||
|
||||
# --- Helper Functions ---
|
||||
|
||||
def _get_download_dir():
|
||||
@ -134,6 +141,62 @@ def parse_package_filename(filename):
|
||||
version = ""
|
||||
return name, version
|
||||
|
||||
def remove_narrative(resource):
|
||||
"""Remove narrative text element from a FHIR resource."""
|
||||
if isinstance(resource, dict):
|
||||
if 'text' in resource:
|
||||
logger.debug(f"Removing narrative text from resource: {resource.get('resourceType', 'unknown')}")
|
||||
del resource['text']
|
||||
if resource.get('resourceType') == 'Bundle' and 'entry' in resource:
|
||||
resource['entry'] = [dict(entry, resource=remove_narrative(entry.get('resource'))) if entry.get('resource') else entry for entry in resource['entry']]
|
||||
return resource
|
||||
|
||||
def get_cached_structure(package_name, package_version, resource_type, view):
|
||||
"""Retrieve cached StructureDefinition from SQLite."""
|
||||
try:
|
||||
conn = sqlite3.connect(os.path.join(current_app.instance_path, 'fhir_ig.db'))
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT structure_data FROM structure_cache
|
||||
WHERE package_name = ? AND package_version = ? AND resource_type = ? AND view = ?
|
||||
""", (package_name, package_version, resource_type, view))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
if result:
|
||||
logger.debug(f"Cache hit for {package_name}#{package_version}:{resource_type}:{view}")
|
||||
return json.loads(result[0])
|
||||
logger.debug(f"No cache entry for {package_name}#{package_version}:{resource_type}:{view}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error accessing structure cache: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def cache_structure(package_name, package_version, resource_type, view, structure_data):
|
||||
"""Cache StructureDefinition in SQLite."""
|
||||
try:
|
||||
conn = sqlite3.connect(os.path.join(current_app.instance_path, 'fhir_ig.db'))
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS structure_cache (
|
||||
package_name TEXT,
|
||||
package_version TEXT,
|
||||
resource_type TEXT,
|
||||
view TEXT,
|
||||
structure_data TEXT,
|
||||
PRIMARY KEY (package_name, package_version, resource_type, view)
|
||||
)
|
||||
""")
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO structure_cache
|
||||
(package_name, package_version, resource_type, view, structure_data)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (package_name, package_version, resource_type, view, json.dumps(structure_data)))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.debug(f"Cached structure for {package_name}#{package_version}:{resource_type}:{view}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error caching structure: {e}", exc_info=True)
|
||||
|
||||
def find_and_extract_sd(tgz_path, resource_identifier, profile_url=None):
|
||||
"""Helper to find and extract StructureDefinition json from a tgz path, prioritizing profile match."""
|
||||
sd_data = None
|
||||
@ -144,45 +207,84 @@ def find_and_extract_sd(tgz_path, resource_identifier, profile_url=None):
|
||||
try:
|
||||
with tarfile.open(tgz_path, "r:gz") as tar:
|
||||
logger.debug(f"Searching for SD matching '{resource_identifier}' with profile '{profile_url}' in {os.path.basename(tgz_path)}")
|
||||
# Store potential matches to evaluate the best one at the end
|
||||
potential_matches = [] # Store tuples of (precision_score, data, member_name)
|
||||
|
||||
for member in tar:
|
||||
if not (member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json')):
|
||||
continue
|
||||
# Skip common metadata files
|
||||
if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']:
|
||||
continue
|
||||
|
||||
fileobj = None
|
||||
try:
|
||||
fileobj = tar.extractfile(member)
|
||||
if fileobj:
|
||||
content_bytes = fileobj.read()
|
||||
# Handle potential BOM (Byte Order Mark)
|
||||
content_string = content_bytes.decode('utf-8-sig')
|
||||
data = json.loads(content_string)
|
||||
|
||||
if isinstance(data, dict) and data.get('resourceType') == 'StructureDefinition':
|
||||
sd_id = data.get('id')
|
||||
sd_name = data.get('name')
|
||||
sd_type = data.get('type')
|
||||
sd_url = data.get('url')
|
||||
# Log SD details for debugging
|
||||
logger.debug(f"Found SD: id={sd_id}, name={sd_name}, type={sd_type}, url={sd_url}, path={member.name}")
|
||||
# Prioritize match with profile_url if provided
|
||||
sd_filename_base = os.path.splitext(os.path.basename(member.name))[0]
|
||||
sd_filename_lower = sd_filename_base.lower()
|
||||
resource_identifier_lower = resource_identifier.lower() if resource_identifier else None
|
||||
|
||||
# logger.debug(f"Checking SD: id={sd_id}, name={sd_name}, type={sd_type}, url={sd_url}, file={sd_filename_lower} against identifier='{resource_identifier}'")
|
||||
|
||||
match_score = 0 # Higher score means more precise match
|
||||
|
||||
# Highest precision: Exact match on profile_url
|
||||
if profile_url and sd_url == profile_url:
|
||||
sd_data = data
|
||||
match_score = 5
|
||||
logger.debug(f"Exact match found based on profile_url: {profile_url}")
|
||||
# If we find the exact profile URL, this is the best possible match.
|
||||
sd_data = remove_narrative(data)
|
||||
found_path = member.name
|
||||
logger.info(f"Found definitive SD matching profile '{profile_url}' at path: {found_path}. Stopping search.")
|
||||
break # Stop searching immediately
|
||||
|
||||
# Next highest precision: Exact match on id or name
|
||||
elif resource_identifier_lower:
|
||||
if sd_id and resource_identifier_lower == sd_id.lower():
|
||||
match_score = 4
|
||||
logger.debug(f"Match found based on exact sd_id: {sd_id}")
|
||||
elif sd_name and resource_identifier_lower == sd_name.lower():
|
||||
match_score = 4
|
||||
logger.debug(f"Match found based on exact sd_name: {sd_name}")
|
||||
# Next: Match filename pattern "StructureDefinition-{identifier}.json"
|
||||
elif sd_filename_lower == f"structuredefinition-{resource_identifier_lower}":
|
||||
match_score = 3
|
||||
logger.debug(f"Match found based on exact filename pattern: {member.name}")
|
||||
# Next: Match on type ONLY if the identifier looks like a base type (no hyphens/dots)
|
||||
elif sd_type and resource_identifier_lower == sd_type.lower() and not re.search(r'[-.]', resource_identifier):
|
||||
match_score = 2
|
||||
logger.debug(f"Match found based on sd_type (simple identifier): {sd_type}")
|
||||
# Lower precision: Check if identifier is IN the filename
|
||||
elif resource_identifier_lower in sd_filename_lower:
|
||||
match_score = 1
|
||||
logger.debug(f"Potential match based on identifier in filename: {member.name}")
|
||||
# Lowest precision: Check if identifier is IN the URL
|
||||
elif sd_url and resource_identifier_lower in sd_url.lower():
|
||||
match_score = 1
|
||||
logger.debug(f"Potential match based on identifier in url: {sd_url}")
|
||||
|
||||
if match_score > 0:
|
||||
potential_matches.append((match_score, remove_narrative(data), member.name))
|
||||
|
||||
# If it's a very high precision match, we can potentially break early
|
||||
if match_score >= 3: # Exact ID, Name, or Filename pattern
|
||||
logger.info(f"Found high-confidence match for '{resource_identifier}' ({member.name}), stopping search.")
|
||||
# Set sd_data here and break
|
||||
sd_data = remove_narrative(data)
|
||||
found_path = member.name
|
||||
logger.info(f"Found SD matching profile '{profile_url}' at path: {found_path}")
|
||||
break
|
||||
# Broader matching for resource_identifier
|
||||
elif resource_identifier and (
|
||||
(sd_id and resource_identifier.lower() == sd_id.lower()) or
|
||||
(sd_name and resource_identifier.lower() == sd_name.lower()) or
|
||||
(sd_type and resource_identifier.lower() == sd_type.lower()) or
|
||||
# Add fallback for partial filename match
|
||||
(resource_identifier.lower() in os.path.splitext(os.path.basename(member.name))[0].lower()) or
|
||||
# Handle AU Core naming conventions
|
||||
(sd_url and resource_identifier.lower() in sd_url.lower())
|
||||
):
|
||||
sd_data = data
|
||||
found_path = member.name
|
||||
logger.info(f"Found matching SD for '{resource_identifier}' at path: {found_path}")
|
||||
# Continue searching for a profile match
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.debug(f"Could not parse JSON in {member.name}, skipping: {e}")
|
||||
except UnicodeDecodeError as e:
|
||||
@ -194,20 +296,32 @@ def find_and_extract_sd(tgz_path, resource_identifier, profile_url=None):
|
||||
finally:
|
||||
if fileobj:
|
||||
fileobj.close()
|
||||
|
||||
# If the loop finished without finding an exact profile_url or high-confidence match (score >= 3)
|
||||
if not sd_data and potential_matches:
|
||||
# Sort potential matches by score (highest first)
|
||||
potential_matches.sort(key=lambda x: x[0], reverse=True)
|
||||
best_match = potential_matches[0]
|
||||
sd_data = best_match[1]
|
||||
found_path = best_match[2]
|
||||
logger.info(f"Selected best match for '{resource_identifier}' from potential matches (Score: {best_match[0]}): {found_path}")
|
||||
|
||||
if sd_data is None:
|
||||
logger.info(f"SD matching identifier '{resource_identifier}' or profile '{profile_url}' not found within archive {os.path.basename(tgz_path)}")
|
||||
|
||||
except tarfile.ReadError as e:
|
||||
logger.error(f"Tar ReadError reading {tgz_path}: {e}")
|
||||
return None, None
|
||||
except tarfile.TarError as e:
|
||||
logger.error(f"TarError reading {tgz_path} in find_and_extract_sd: {e}")
|
||||
raise
|
||||
raise # Re-raise critical tar errors
|
||||
except FileNotFoundError:
|
||||
logger.error(f"FileNotFoundError reading {tgz_path} in find_and_extract_sd.")
|
||||
raise
|
||||
raise # Re-raise critical file errors
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in find_and_extract_sd for {tgz_path}: {e}", exc_info=True)
|
||||
raise
|
||||
raise # Re-raise unexpected errors
|
||||
|
||||
return sd_data, found_path
|
||||
|
||||
# --- Metadata Saving/Loading ---
|
||||
@ -263,7 +377,6 @@ def get_package_metadata(name, version):
|
||||
else:
|
||||
logger.debug(f"Metadata file not found: {metadata_path}")
|
||||
return None
|
||||
# --- Package Processing ---
|
||||
|
||||
def process_package_file(tgz_path):
|
||||
"""Extracts types, profile status, MS elements, examples, and profile relationships from a downloaded .tgz package."""
|
||||
@ -272,6 +385,7 @@ def process_package_file(tgz_path):
|
||||
return {'errors': [f"Package file not found: {tgz_path}"], 'resource_types_info': []}
|
||||
|
||||
pkg_basename = os.path.basename(tgz_path)
|
||||
name, version = parse_package_filename(pkg_basename)
|
||||
logger.info(f"Processing package file details: {pkg_basename}")
|
||||
|
||||
results = {
|
||||
@ -320,6 +434,9 @@ def process_package_file(tgz_path):
|
||||
if not isinstance(data, dict) or data.get('resourceType') != 'StructureDefinition':
|
||||
continue
|
||||
|
||||
# Remove narrative text element
|
||||
data = remove_narrative(data)
|
||||
|
||||
profile_id = data.get('id') or data.get('name')
|
||||
sd_type = data.get('type')
|
||||
sd_base = data.get('baseDefinition')
|
||||
@ -342,6 +459,34 @@ def process_package_file(tgz_path):
|
||||
entry['sd_processed'] = True
|
||||
referenced_types.add(sd_type)
|
||||
|
||||
# Cache StructureDefinition for all views
|
||||
views = ['differential', 'snapshot', 'must-support', 'key-elements']
|
||||
for view in views:
|
||||
view_elements = data.get('differential', {}).get('element', []) if view == 'differential' else data.get('snapshot', {}).get('element', [])
|
||||
if view == 'must-support':
|
||||
view_elements = [e for e in view_elements if e.get('mustSupport')]
|
||||
elif view == 'key-elements':
|
||||
key_elements = set()
|
||||
differential_elements = data.get('differential', {}).get('element', [])
|
||||
for e in view_elements:
|
||||
is_in_differential = any(de['id'] == e['id'] for de in differential_elements)
|
||||
if e.get('mustSupport') or is_in_differential or e.get('min', 0) > 0 or e.get('max') != '*' or e.get('slicing') or e.get('constraint'):
|
||||
key_elements.add(e['id'])
|
||||
parent_path = e['path']
|
||||
while '.' in parent_path:
|
||||
parent_path = parent_path.rsplit('.', 1)[0]
|
||||
parent_element = next((el for el in view_elements if el['path'] == parent_path), None)
|
||||
if parent_element:
|
||||
key_elements.add(parent_element['id'])
|
||||
view_elements = [e for e in view_elements if e['id'] in key_elements]
|
||||
cache_data = {
|
||||
'structure_definition': data,
|
||||
'must_support_paths': [],
|
||||
'fallback_used': False,
|
||||
'source_package': f"{name}#{version}"
|
||||
}
|
||||
cache_structure(name, version, sd_type, view, cache_data)
|
||||
|
||||
complies_with = []
|
||||
imposed = []
|
||||
for ext in data.get('extension', []):
|
||||
@ -372,7 +517,7 @@ def process_package_file(tgz_path):
|
||||
|
||||
if must_support is True:
|
||||
if element_id and element_path:
|
||||
ms_path = element_id if slice_name else element_path
|
||||
ms_path = f"{element_path}[sliceName='{slice_name}']" if slice_name else element_id
|
||||
ms_paths_in_this_sd.add(ms_path)
|
||||
has_ms_in_this_sd = True
|
||||
logger.info(f"Found MS element in {entry_key}: path={element_path}, id={element_id}, sliceName={slice_name}, ms_path={ms_path}")
|
||||
@ -432,6 +577,9 @@ def process_package_file(tgz_path):
|
||||
resource_type = data.get('resourceType')
|
||||
if not resource_type: continue
|
||||
|
||||
# Remove narrative text element
|
||||
data = remove_narrative(data)
|
||||
|
||||
profile_meta = data.get('meta', {}).get('profile', [])
|
||||
found_profile_match = False
|
||||
if profile_meta and isinstance(profile_meta, list):
|
||||
@ -570,7 +718,8 @@ def process_package_file(tgz_path):
|
||||
return results
|
||||
|
||||
# --- Validation Functions ---
|
||||
def navigate_fhir_path(resource, path, extension_url=None):
|
||||
|
||||
def _legacy_navigate_fhir_path(resource, path, extension_url=None):
|
||||
"""Navigates a FHIR resource using a FHIRPath-like expression, handling nested structures."""
|
||||
logger.debug(f"Navigating FHIR path: {path}")
|
||||
if not resource or not path:
|
||||
@ -646,7 +795,24 @@ def navigate_fhir_path(resource, path, extension_url=None):
|
||||
logger.debug(f"Path {path} resolved to: {result}")
|
||||
return result
|
||||
|
||||
def validate_resource_against_profile(package_name, version, resource, include_dependencies=True):
|
||||
def navigate_fhir_path(resource, path, extension_url=None):
|
||||
"""Navigates a FHIR resource using FHIRPath expressions."""
|
||||
logger.debug(f"Navigating FHIR path: {path}, extension_url={extension_url}")
|
||||
if not resource or not path:
|
||||
return None
|
||||
try:
|
||||
# Adjust path for extension filtering
|
||||
if extension_url and 'extension' in path:
|
||||
path = f"{path}[url='{extension_url}']"
|
||||
result = evaluate(resource, path)
|
||||
# Return first result if list, None if empty
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
logger.error(f"FHIRPath evaluation failed for {path}: {e}")
|
||||
# Fallback to legacy navigation for compatibility
|
||||
return _legacy_navigate_fhir_path(resource, path, extension_url)
|
||||
|
||||
def _legacy_validate_resource_against_profile(package_name, version, resource, include_dependencies=True):
|
||||
"""Validates a FHIR resource against a StructureDefinition in the specified package."""
|
||||
logger.debug(f"Validating resource {resource.get('resourceType')} against {package_name}#{version}, include_dependencies={include_dependencies}")
|
||||
result = {
|
||||
@ -750,7 +916,7 @@ def validate_resource_against_profile(package_name, version, resource, include_d
|
||||
definition = element.get('definition', 'No definition provided in StructureDefinition.')
|
||||
|
||||
# Check required elements
|
||||
if min_val > 0 and not '.' in path[1 + path.find('.'):]:
|
||||
if min_val > 0 and not '.' in path[1 + path.find('.'):] if path.find('.') != -1 else True:
|
||||
value = navigate_fhir_path(resource, path)
|
||||
if value is None or (isinstance(value, list) and not any(value)):
|
||||
error_msg = f"{resource.get('resourceType')}/{resource.get('id', 'unknown')}: Required element {path} missing"
|
||||
@ -763,7 +929,7 @@ def validate_resource_against_profile(package_name, version, resource, include_d
|
||||
logger.info(f"Validation error: Required element {path} missing")
|
||||
|
||||
# Check must-support elements
|
||||
if must_support and not '.' in path[1 + path.find('.'):]:
|
||||
if must_support and not '.' in path[1 + path.find('.'):] if path.find('.') != -1 else True:
|
||||
if '[x]' in path:
|
||||
base_path = path.replace('[x]', '')
|
||||
found = False
|
||||
@ -827,6 +993,145 @@ def validate_resource_against_profile(package_name, version, resource, include_d
|
||||
logger.debug(f"Validation result: valid={result['valid']}, errors={len(result['errors'])}, warnings={len(result['warnings'])}")
|
||||
return result
|
||||
|
||||
def validate_resource_against_profile(package_name, version, resource, include_dependencies=True):
|
||||
result = {
|
||||
'valid': True,
|
||||
'errors': [],
|
||||
'warnings': [],
|
||||
'details': [],
|
||||
'resource_type': resource.get('resourceType'),
|
||||
'resource_id': resource.get('id', 'unknown'),
|
||||
'profile': resource.get('meta', {}).get('profile', [None])[0]
|
||||
}
|
||||
|
||||
# Attempt HAPI validation if a profile is specified
|
||||
if result['profile']:
|
||||
try:
|
||||
hapi_url = f"http://localhost:8080/fhir/{resource['resourceType']}/$validate?profile={result['profile']}"
|
||||
response = requests.post(
|
||||
hapi_url,
|
||||
json=resource,
|
||||
headers={'Content-Type': 'application/fhir+json', 'Accept': 'application/fhir+json'},
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
outcome = response.json()
|
||||
if outcome.get('resourceType') == 'OperationOutcome':
|
||||
for issue in outcome.get('issue', []):
|
||||
severity = issue.get('severity')
|
||||
diagnostics = issue.get('diagnostics', issue.get('details', {}).get('text', 'No details provided'))
|
||||
detail = {
|
||||
'issue': diagnostics,
|
||||
'severity': severity,
|
||||
'description': issue.get('details', {}).get('text', diagnostics)
|
||||
}
|
||||
if severity in ['error', 'fatal']:
|
||||
result['valid'] = False
|
||||
result['errors'].append(diagnostics)
|
||||
elif severity == 'warning':
|
||||
result['warnings'].append(diagnostics)
|
||||
result['details'].append(detail)
|
||||
result['summary'] = {
|
||||
'error_count': len(result['errors']),
|
||||
'warning_count': len(result['warnings'])
|
||||
}
|
||||
logger.debug(f"HAPI validation for {result['resource_type']}/{result['resource_id']}: valid={result['valid']}, errors={len(result['errors'])}, warnings={len(result['warnings'])}")
|
||||
return result
|
||||
else:
|
||||
logger.warning(f"HAPI returned non-OperationOutcome: {outcome.get('resourceType')}")
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"HAPI validation failed for {result['resource_type']}/{result['resource_id']}: {e}")
|
||||
result['details'].append({
|
||||
'issue': f"HAPI validation failed: {str(e)}",
|
||||
'severity': 'warning',
|
||||
'description': 'Falling back to local validation due to HAPI server error.'
|
||||
})
|
||||
|
||||
# Fallback to local validation
|
||||
download_dir = _get_download_dir()
|
||||
if not download_dir:
|
||||
result['valid'] = False
|
||||
result['errors'].append("Could not access download directory")
|
||||
result['details'].append({
|
||||
'issue': "Could not access download directory",
|
||||
'severity': 'error',
|
||||
'description': "The server could not locate the directory where FHIR packages are stored."
|
||||
})
|
||||
return result
|
||||
|
||||
tgz_path = os.path.join(download_dir, construct_tgz_filename(package_name, version))
|
||||
sd_data, sd_path = find_and_extract_sd(tgz_path, resource.get('resourceType'), result['profile'])
|
||||
if not sd_data:
|
||||
result['valid'] = False
|
||||
result['errors'].append(f"No StructureDefinition found for {resource.get('resourceType')}")
|
||||
result['details'].append({
|
||||
'issue': f"No StructureDefinition found for {resource.get('resourceType')}",
|
||||
'severity': 'error',
|
||||
'description': f"The package {package_name}#{version} does not contain a matching StructureDefinition."
|
||||
})
|
||||
return result
|
||||
|
||||
elements = sd_data.get('snapshot', {}).get('element', [])
|
||||
for element in elements:
|
||||
path = element.get('path')
|
||||
min_val = element.get('min', 0)
|
||||
must_support = element.get('mustSupport', False)
|
||||
slicing = element.get('slicing')
|
||||
slice_name = element.get('sliceName')
|
||||
|
||||
# Check required elements
|
||||
if min_val > 0:
|
||||
value = navigate_fhir_path(resource, path)
|
||||
if value is None or (isinstance(value, list) and not any(value)):
|
||||
result['valid'] = False
|
||||
result['errors'].append(f"Required element {path} missing")
|
||||
result['details'].append({
|
||||
'issue': f"Required element {path} missing",
|
||||
'severity': 'error',
|
||||
'description': f"Element {path} has min={min_val} in profile {result['profile'] or 'unknown'}"
|
||||
})
|
||||
|
||||
# Check must-support elements
|
||||
if must_support:
|
||||
value = navigate_fhir_path(resource, slice_name if slice_name else path)
|
||||
if value is None or (isinstance(value, list) and not any(value)):
|
||||
result['warnings'].append(f"Must Support element {path} missing or empty")
|
||||
result['details'].append({
|
||||
'issue': f"Must Support element {path} missing or empty",
|
||||
'severity': 'warning',
|
||||
'description': f"Element {path} is marked as Must Support in profile {result['profile'] or 'unknown'}"
|
||||
})
|
||||
|
||||
# Validate slicing
|
||||
if slicing and not slice_name: # Parent slicing element
|
||||
discriminator = slicing.get('discriminator', [])
|
||||
for d in discriminator:
|
||||
d_type = d.get('type')
|
||||
d_path = d.get('path')
|
||||
if d_type == 'value':
|
||||
sliced_elements = navigate_fhir_path(resource, path)
|
||||
if isinstance(sliced_elements, list):
|
||||
seen_values = set()
|
||||
for elem in sliced_elements:
|
||||
d_value = navigate_fhir_path(elem, d_path)
|
||||
if d_value in seen_values:
|
||||
result['valid'] = False
|
||||
result['errors'].append(f"Duplicate discriminator value {d_value} for {path}.{d_path}")
|
||||
seen_values.add(d_value)
|
||||
elif d_type == 'type':
|
||||
sliced_elements = navigate_fhir_path(resource, path)
|
||||
if isinstance(sliced_elements, list):
|
||||
for elem in sliced_elements:
|
||||
if not navigate_fhir_path(elem, d_path):
|
||||
result['valid'] = False
|
||||
result['errors'].append(f"Missing discriminator type {d_path} for {path}")
|
||||
|
||||
result['summary'] = {
|
||||
'error_count': len(result['errors']),
|
||||
'warning_count': len(result['warnings'])
|
||||
}
|
||||
return result
|
||||
|
||||
def validate_bundle_against_profile(package_name, version, bundle, include_dependencies=True):
|
||||
"""Validates a FHIR Bundle against profiles in the specified package."""
|
||||
logger.debug(f"Validating bundle against {package_name}#{version}, include_dependencies={include_dependencies}")
|
||||
@ -950,7 +1255,7 @@ def get_structure_definition(package_name, version, resource_type):
|
||||
element_id = element.get('id', '')
|
||||
slice_name = element.get('sliceName')
|
||||
if element.get('mustSupport', False):
|
||||
ms_path = element_id if slice_name else path
|
||||
ms_path = f"{path}[sliceName='{slice_name}']" if slice_name else element_id
|
||||
must_support_paths.append(ms_path)
|
||||
if 'slicing' in element:
|
||||
slice_info = {
|
||||
@ -1384,6 +1689,250 @@ def validate_sample():
|
||||
'warnings': [],
|
||||
'results': {}
|
||||
}), 500
|
||||
|
||||
def run_gofsh(input_path, output_dir, output_style, log_level, fhir_version=None, fishing_trip=False, dependencies=None, indent_rules=False, meta_profile='only-one', alias_file=None, no_alias=False):
|
||||
"""Run GoFSH with advanced options and return FSH output and optional comparison report."""
|
||||
# Use a temporary output directory for initial GoFSH run
|
||||
temp_output_dir = tempfile.mkdtemp()
|
||||
os.chmod(temp_output_dir, 0o777)
|
||||
|
||||
cmd = ["gofsh", input_path, "-o", temp_output_dir, "-s", output_style, "-l", log_level]
|
||||
if fhir_version:
|
||||
cmd.extend(["-u", fhir_version])
|
||||
if dependencies:
|
||||
for dep in dependencies:
|
||||
cmd.extend(["--dependency", dep.strip()])
|
||||
if indent_rules:
|
||||
cmd.append("--indent")
|
||||
if no_alias:
|
||||
cmd.append("--no-alias")
|
||||
if alias_file:
|
||||
cmd.extend(["--alias-file", alias_file])
|
||||
if meta_profile != 'only-one':
|
||||
cmd.extend(["--meta-profile", meta_profile])
|
||||
|
||||
# Set environment to disable TTY interactions
|
||||
env = os.environ.copy()
|
||||
env["NODE_NO_READLINE"] = "1"
|
||||
env["NODE_NO_INTERACTIVE"] = "1"
|
||||
env["TERM"] = "dumb"
|
||||
env["CI"] = "true"
|
||||
env["FORCE_COLOR"] = "0"
|
||||
env["NODE_ENV"] = "production"
|
||||
|
||||
# Create a wrapper script in /tmp
|
||||
wrapper_script = "/tmp/gofsh_wrapper.sh"
|
||||
output_file = "/tmp/gofsh_output.log"
|
||||
try:
|
||||
with open(wrapper_script, 'w') as f:
|
||||
f.write("#!/bin/bash\n")
|
||||
# Redirect /dev/tty writes to /dev/null
|
||||
f.write("exec 3>/dev/null\n")
|
||||
f.write(" ".join([f'"{arg}"' for arg in cmd]) + f" </dev/null >{output_file} 2>&1\n")
|
||||
os.chmod(wrapper_script, 0o755)
|
||||
|
||||
# Log the wrapper script contents for debugging
|
||||
with open(wrapper_script, 'r') as f:
|
||||
logger.debug(f"Wrapper script contents:\n{f.read()}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create wrapper script {wrapper_script}: {str(e)}", exc_info=True)
|
||||
return None, None, f"Failed to create wrapper script: {str(e)}"
|
||||
|
||||
try:
|
||||
# Log directory contents before execution
|
||||
logger.debug(f"Temp output directory contents before GoFSH: {os.listdir(temp_output_dir)}")
|
||||
|
||||
result = subprocess.run(
|
||||
[wrapper_script],
|
||||
check=True,
|
||||
env=env
|
||||
)
|
||||
# Read output from the log file
|
||||
with open(output_file, 'r', encoding='utf-8') as f:
|
||||
output = f.read()
|
||||
logger.debug(f"GoFSH output:\n{output}")
|
||||
|
||||
# Prepare final output directory
|
||||
if os.path.exists(output_dir):
|
||||
shutil.rmtree(output_dir)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
os.chmod(output_dir, 0o777)
|
||||
|
||||
# Copy .fsh files, sushi-config.yaml, and input JSON to final output directory
|
||||
copied_files = []
|
||||
for root, _, files in os.walk(temp_output_dir):
|
||||
for file in files:
|
||||
src_path = os.path.join(root, file)
|
||||
if file.endswith(".fsh") or file == "sushi-config.yaml":
|
||||
relative_path = os.path.relpath(src_path, temp_output_dir)
|
||||
dst_path = os.path.join(output_dir, relative_path)
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
shutil.copy2(src_path, dst_path)
|
||||
copied_files.append(relative_path)
|
||||
|
||||
# Copy input JSON to final directory
|
||||
input_filename = os.path.basename(input_path)
|
||||
dst_input_path = os.path.join(output_dir, "input", input_filename)
|
||||
os.makedirs(os.path.dirname(dst_input_path), exist_ok=True)
|
||||
shutil.copy2(input_path, dst_input_path)
|
||||
copied_files.append(os.path.join("input", input_filename))
|
||||
|
||||
# Create a minimal sushi-config.yaml if missing
|
||||
sushi_config_path = os.path.join(output_dir, "sushi-config.yaml")
|
||||
if not os.path.exists(sushi_config_path):
|
||||
minimal_config = {
|
||||
"id": "fhirflare.temp",
|
||||
"canonical": "http://fhirflare.org",
|
||||
"name": "FHIRFLARETempIG",
|
||||
"version": "0.1.0",
|
||||
"fhirVersion": fhir_version or "4.0.1",
|
||||
"FSHOnly": True,
|
||||
"dependencies": dependencies or []
|
||||
}
|
||||
with open(sushi_config_path, 'w') as f:
|
||||
json.dump(minimal_config, f, indent=2)
|
||||
copied_files.append("sushi-config.yaml")
|
||||
|
||||
# Run GoFSH with --fshing-trip in a fresh temporary directory
|
||||
comparison_report = None
|
||||
if fishing_trip:
|
||||
fishing_temp_dir = tempfile.mkdtemp()
|
||||
os.chmod(fishing_temp_dir, 0o777)
|
||||
gofsh_fishing_cmd = ["gofsh", input_path, "-o", fishing_temp_dir, "-s", output_style, "-l", log_level, "--fshing-trip"]
|
||||
if fhir_version:
|
||||
gofsh_fishing_cmd.extend(["-u", fhir_version])
|
||||
if dependencies:
|
||||
for dep in dependencies:
|
||||
gofsh_fishing_cmd.extend(["--dependency", dep.strip()])
|
||||
if indent_rules:
|
||||
gofsh_fishing_cmd.append("--indent")
|
||||
if no_alias:
|
||||
gofsh_fishing_cmd.append("--no-alias")
|
||||
if alias_file:
|
||||
gofsh_fishing_cmd.extend(["--alias-file", alias_file])
|
||||
if meta_profile != 'only-one':
|
||||
gofsh_fishing_cmd.extend(["--meta-profile", meta_profile])
|
||||
|
||||
try:
|
||||
with open(wrapper_script, 'w') as f:
|
||||
f.write("#!/bin/bash\n")
|
||||
f.write("exec 3>/dev/null\n")
|
||||
f.write("exec >/dev/null 2>&1\n") # Suppress all output to /dev/tty
|
||||
f.write(" ".join([f'"{arg}"' for arg in gofsh_fishing_cmd]) + f" </dev/null >{output_file} 2>&1\n")
|
||||
os.chmod(wrapper_script, 0o755)
|
||||
|
||||
logger.debug(f"GoFSH fishing-trip wrapper script contents:\n{open(wrapper_script, 'r').read()}")
|
||||
|
||||
result = subprocess.run(
|
||||
[wrapper_script],
|
||||
check=True,
|
||||
env=env
|
||||
)
|
||||
with open(output_file, 'r', encoding='utf-8') as f:
|
||||
fishing_output = f.read()
|
||||
logger.debug(f"GoFSH fishing-trip output:\n{fishing_output}")
|
||||
|
||||
# Copy fshing-trip-comparison.html to final directory
|
||||
for root, _, files in os.walk(fishing_temp_dir):
|
||||
for file in files:
|
||||
if file.endswith(".html") and "fshing-trip-comparison" in file.lower():
|
||||
src_path = os.path.join(root, file)
|
||||
dst_path = os.path.join(output_dir, file)
|
||||
shutil.copy2(src_path, dst_path)
|
||||
copied_files.append(file)
|
||||
with open(dst_path, 'r', encoding='utf-8') as f:
|
||||
comparison_report = f.read()
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_output = ""
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r', encoding='utf-8') as f:
|
||||
error_output = f.read()
|
||||
logger.error(f"GoFSH fishing-trip failed: {error_output}")
|
||||
return None, None, f"GoFSH fishing-trip failed: {error_output}"
|
||||
finally:
|
||||
if os.path.exists(fishing_temp_dir):
|
||||
shutil.rmtree(fishing_temp_dir, ignore_errors=True)
|
||||
|
||||
# Read FSH files from final output directory
|
||||
fsh_content = []
|
||||
for root, _, files in os.walk(output_dir):
|
||||
for file in files:
|
||||
if file.endswith(".fsh"):
|
||||
with open(os.path.join(root, file), 'r', encoding='utf-8') as f:
|
||||
fsh_content.append(f.read())
|
||||
fsh_output = "\n\n".join(fsh_content)
|
||||
|
||||
# Log copied files
|
||||
logger.debug(f"Copied files to final output directory: {copied_files}")
|
||||
|
||||
logger.info(f"GoFSH executed successfully for {input_path}")
|
||||
return fsh_output, comparison_report, None
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_output = ""
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r', encoding='utf-8') as f:
|
||||
error_output = f.read()
|
||||
logger.error(f"GoFSH failed: {error_output}")
|
||||
return None, None, f"GoFSH failed: {error_output}"
|
||||
except Exception as e:
|
||||
logger.error(f"Error running GoFSH: {str(e)}", exc_info=True)
|
||||
return None, None, f"Error running GoFSH: {str(e)}"
|
||||
finally:
|
||||
# Clean up temporary files
|
||||
if os.path.exists(wrapper_script):
|
||||
os.remove(wrapper_script)
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
if os.path.exists(temp_output_dir):
|
||||
shutil.rmtree(temp_output_dir, ignore_errors=True)
|
||||
|
||||
def process_fhir_input(input_mode, fhir_file, fhir_text, alias_file=None):
|
||||
"""Process user input (file or text) and save to temporary files."""
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
input_file = None
|
||||
alias_path = None
|
||||
|
||||
try:
|
||||
if input_mode == 'file' and fhir_file:
|
||||
content = fhir_file.read().decode('utf-8')
|
||||
file_type = 'json' if content.strip().startswith('{') else 'xml'
|
||||
input_file = os.path.join(temp_dir, f"input.{file_type}")
|
||||
with open(input_file, 'w') as f:
|
||||
f.write(content)
|
||||
elif input_mode == 'text' and fhir_text:
|
||||
content = fhir_text.strip()
|
||||
file_type = 'json' if content.strip().startswith('{') else 'xml'
|
||||
input_file = os.path.join(temp_dir, f"input.{file_type}")
|
||||
with open(input_file, 'w') as f:
|
||||
f.write(content)
|
||||
else:
|
||||
return None, None, None, "No input provided"
|
||||
|
||||
# Basic validation
|
||||
if file_type == 'json':
|
||||
try:
|
||||
json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return None, None, None, "Invalid JSON format"
|
||||
elif file_type == 'xml':
|
||||
try:
|
||||
ET.fromstring(content)
|
||||
except ET.ParseError:
|
||||
return None, None, None, "Invalid XML format"
|
||||
|
||||
# Process alias file if provided
|
||||
if alias_file:
|
||||
alias_content = alias_file.read().decode('utf-8')
|
||||
alias_path = os.path.join(temp_dir, "aliases.fsh")
|
||||
with open(alias_path, 'w') as f:
|
||||
f.write(alias_content)
|
||||
|
||||
logger.debug(f"Processed input: {(input_file, alias_path)}")
|
||||
return input_file, temp_dir, alias_path, None
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing input: {str(e)}", exc_info=True)
|
||||
return None, None, None, f"Error processing input: {str(e)}"
|
||||
|
||||
# --- Standalone Test ---
|
||||
if __name__ == '__main__':
|
||||
logger.info("Running services.py directly for testing.")
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 449 KiB |
BIN
static/FHIRFLARE.png.old
Normal file
BIN
static/FHIRFLARE.png.old
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
1
static/animations/loading-dark.json
Normal file
1
static/animations/loading-dark.json
Normal file
File diff suppressed because one or more lines are too long
49070
static/animations/loading-light.json
Normal file
49070
static/animations/loading-light.json
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 222 KiB |
BIN
static/favicon.ico.old
Normal file
BIN
static/favicon.ico.old
Normal file
Binary file not shown.
After Width: | Height: | Size: 281 KiB |
1
static/js/lottie.min.js
vendored
Normal file
1
static/js/lottie.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
299
static/uploads/fsh_output/fshing-trip-comparison.html
Normal file
299
static/uploads/fsh_output/fshing-trip-comparison.html
Normal file
@ -0,0 +1,299 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>FSHing Trip Comparison</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/github.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const targetElement = document.getElementById('diff');
|
||||
const diff2htmlUi = new Diff2HtmlUI(targetElement);
|
||||
diff2htmlUi.fileListToggle(false);
|
||||
diff2htmlUi.synchronisedScroll();
|
||||
diff2htmlUi.highlightCode();
|
||||
const diffs = document.getElementsByClassName('d2h-file-wrapper');
|
||||
for (const diff of diffs) {
|
||||
diff.innerHTML = `
|
||||
<details>
|
||||
<summary>${diff.getElementsByClassName('d2h-file-name')[0].innerHTML}</summary>
|
||||
${diff.innerHTML}
|
||||
</details>`
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body style="text-align: center; font-family: 'Source Sans Pro', sans-serif">
|
||||
<h1>FSHing Trip Comparison</a></h1>
|
||||
|
||||
<div id="diff">
|
||||
<div class="d2h-file-list-wrapper d2h-light-color-scheme">
|
||||
<div class="d2h-file-list-header">
|
||||
<span class="d2h-file-list-title">Files changed (1)</span>
|
||||
<a class="d2h-file-switch d2h-hide">hide</a>
|
||||
<a class="d2h-file-switch d2h-show">show</a>
|
||||
</div>
|
||||
<ol class="d2h-file-list">
|
||||
<li class="d2h-file-list-line">
|
||||
<span class="d2h-file-name-wrapper">
|
||||
<svg aria-hidden="true" class="d2h-icon d2h-moved" height="16" title="renamed" version="1.1"
|
||||
viewBox="0 0 14 16" width="14">
|
||||
<path d="M6 9H3V7h3V4l5 4-5 4V9z m8-7v12c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h12c0.55 0 1 0.45 1 1z m-1 0H1v12h12V2z"></path>
|
||||
</svg> <a href="#d2h-898460" class="d2h-file-name">../tmp/{tmpkn9pyg0k/input.json → tmpb_q8uf73/fsh-generated/data}/fsh-index.json</a>
|
||||
<span class="d2h-file-stats">
|
||||
<span class="d2h-lines-added">+10</span>
|
||||
<span class="d2h-lines-deleted">-0</span>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div><div class="d2h-wrapper d2h-light-color-scheme">
|
||||
<div id="d2h-898460" class="d2h-file-wrapper" data-lang="json">
|
||||
<div class="d2h-file-header">
|
||||
<span class="d2h-file-name-wrapper">
|
||||
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
|
||||
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
|
||||
</svg> <span class="d2h-file-name">../tmp/{tmpkn9pyg0k/input.json → tmpb_q8uf73/fsh-generated/data}/fsh-index.json</span>
|
||||
<span class="d2h-tag d2h-moved d2h-moved-tag">RENAMED</span></span>
|
||||
<label class="d2h-file-collapse">
|
||||
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
|
||||
Viewed
|
||||
</label>
|
||||
</div>
|
||||
<div class="d2h-files-diff">
|
||||
<div class="d2h-file-side-diff">
|
||||
<div class="d2h-code-wrapper">
|
||||
<table class="d2h-diff-table">
|
||||
<tbody class="d2h-diff-tbody">
|
||||
<tr>
|
||||
<td class="d2h-code-side-linenumber d2h-info"></td>
|
||||
<td class="d2h-info">
|
||||
<div class="d2h-code-side-line">@@ -0,0 +1,10 @@</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
|
||||
|
||||
</td>
|
||||
<td class="d2h-cntx d2h-emptyplaceholder">
|
||||
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
|
||||
<span class="d2h-code-line-prefix"> </span>
|
||||
<span class="d2h-code-line-ctn"><br></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d2h-file-side-diff">
|
||||
<div class="d2h-code-wrapper">
|
||||
<table class="d2h-diff-table">
|
||||
<tbody class="d2h-diff-tbody">
|
||||
<tr>
|
||||
<td class="d2h-code-side-linenumber d2h-info"></td>
|
||||
<td class="d2h-info">
|
||||
<div class="d2h-code-side-line"> </div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
1
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn">[</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
2
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> {</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
3
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> "outputFile": "Encounter-discharge-1.json",</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
4
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> "fshName": "discharge-1",</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
5
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> "fshType": "Instance",</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
6
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> "fshFile": "instances/discharge-1.fsh",</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
7
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> "startLine": 1,</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
8
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> "endLine": 12</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
9
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn"> }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="d2h-code-side-linenumber d2h-ins">
|
||||
10
|
||||
</td>
|
||||
<td class="d2h-ins">
|
||||
<div class="d2h-code-side-line">
|
||||
<span class="d2h-code-line-prefix">+</span>
|
||||
<span class="d2h-code-line-ctn">]</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
static/uploads/fsh_output/input/fsh/aliases.fsh
Normal file
1
static/uploads/fsh_output/input/fsh/aliases.fsh
Normal file
@ -0,0 +1 @@
|
||||
Alias: $v3-ActCode = http://terminology.hl7.org/CodeSystem/v3-ActCode
|
2
static/uploads/fsh_output/input/fsh/index.txt
Normal file
2
static/uploads/fsh_output/input/fsh/index.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Name Type File
|
||||
banks-mia-leanne Instance instances/banks-mia-leanne.fsh
|
16
static/uploads/fsh_output/input/fsh/instances/aspirin.fsh
Normal file
16
static/uploads/fsh_output/input/fsh/instances/aspirin.fsh
Normal file
@ -0,0 +1,16 @@
|
||||
Instance: aspirin
|
||||
InstanceOf: AllergyIntolerance
|
||||
Usage: #example
|
||||
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-allergyintolerance"
|
||||
* clinicalStatus = $allergyintolerance-clinical#active
|
||||
* clinicalStatus.text = "Active"
|
||||
* verificationStatus = $allergyintolerance-verification#confirmed
|
||||
* verificationStatus.text = "Confirmed"
|
||||
* category = #medication
|
||||
* criticality = #unable-to-assess
|
||||
* code = $sct#387458008
|
||||
* code.text = "Aspirin allergy"
|
||||
* patient = Reference(Patient/hayes-arianne)
|
||||
* recordedDate = "2024-02-10"
|
||||
* recorder = Reference(PractitionerRole/specialistphysicians-swanborough-erick)
|
||||
* asserter = Reference(PractitionerRole/specialistphysicians-swanborough-erick)
|
@ -0,0 +1,49 @@
|
||||
Instance: banks-mia-leanne
|
||||
InstanceOf: Patient
|
||||
Usage: #example
|
||||
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient"
|
||||
* extension[0].url = "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity"
|
||||
* extension[=].extension.url = "value"
|
||||
* extension[=].extension.valueCodeableConcept = $sct#446141000124107 "Identifies as female gender"
|
||||
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-pronouns"
|
||||
* extension[=].extension.url = "value"
|
||||
* extension[=].extension.valueCodeableConcept = $loinc#LA29519-8 "she/her/her/hers/herself"
|
||||
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-recordedSexOrGender"
|
||||
* extension[=].extension[0].url = "value"
|
||||
* extension[=].extension[=].valueCodeableConcept = $sct#248152002
|
||||
* extension[=].extension[=].valueCodeableConcept.text = "Female"
|
||||
* extension[=].extension[+].url = "type"
|
||||
* extension[=].extension[=].valueCodeableConcept = $sct#1515311000168102 "Biological sex at birth"
|
||||
* identifier.extension[0].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-status"
|
||||
* identifier.extension[=].valueCoding = $ihi-status-1#active
|
||||
* identifier.extension[+].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-record-status"
|
||||
* identifier.extension[=].valueCoding = $ihi-record-status-1#verified "verified"
|
||||
* identifier.type = $v2-0203#NI
|
||||
* identifier.type.text = "IHI"
|
||||
* identifier.system = "http://ns.electronichealth.net.au/id/hi/ihi/1.0"
|
||||
* identifier.value = "8003608333647261"
|
||||
* name.use = #usual
|
||||
* name.family = "Banks"
|
||||
* name.given[0] = "Mia"
|
||||
* name.given[+] = "Leanne"
|
||||
* telecom[0].system = #phone
|
||||
* telecom[=].value = "0270102724"
|
||||
* telecom[=].use = #work
|
||||
* telecom[+].system = #phone
|
||||
* telecom[=].value = "0491574632"
|
||||
* telecom[=].use = #mobile
|
||||
* telecom[+].system = #phone
|
||||
* telecom[=].value = "0270107520"
|
||||
* telecom[=].use = #home
|
||||
* telecom[+].system = #email
|
||||
* telecom[=].value = "mia.banks@myownpersonaldomain.com"
|
||||
* telecom[+].system = #phone
|
||||
* telecom[=].value = "270107520"
|
||||
* telecom[=].use = #home
|
||||
* gender = #female
|
||||
* birthDate = "1983-08-25"
|
||||
* address.line = "50 Sebastien St"
|
||||
* address.city = "Minjary"
|
||||
* address.state = "NSW"
|
||||
* address.postalCode = "2720"
|
||||
* address.country = "AU"
|
@ -0,0 +1,12 @@
|
||||
Instance: discharge-1
|
||||
InstanceOf: Encounter
|
||||
Usage: #example
|
||||
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-encounter"
|
||||
* status = #finished
|
||||
* class = $v3-ActCode#EMER "emergency"
|
||||
* subject = Reference(Patient/ronny-irvine)
|
||||
* period
|
||||
* start = "2023-02-20T06:15:00+10:00"
|
||||
* end = "2023-02-20T18:19:00+10:00"
|
||||
* location.location = Reference(Location/murrabit-hospital)
|
||||
* serviceProvider = Reference(Organization/murrabit-hospital)
|
15
static/uploads/fsh_output/input/fsh/instances/vkc.fsh
Normal file
15
static/uploads/fsh_output/input/fsh/instances/vkc.fsh
Normal file
@ -0,0 +1,15 @@
|
||||
Instance: vkc
|
||||
InstanceOf: Condition
|
||||
Usage: #example
|
||||
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-condition"
|
||||
* clinicalStatus = $condition-clinical#active "Active"
|
||||
* category = $condition-category#encounter-diagnosis "Encounter Diagnosis"
|
||||
* severity = $sct#24484000 "Severe"
|
||||
* code = $sct#317349009 "Vernal keratoconjunctivitis"
|
||||
* bodySite = $sct#368601006 "Entire conjunctiva of left eye"
|
||||
* subject = Reference(Patient/italia-sofia)
|
||||
* onsetDateTime = "2023-10-01"
|
||||
* recordedDate = "2023-10-02"
|
||||
* recorder = Reference(PractitionerRole/generalpractitioner-guthridge-jarred)
|
||||
* asserter = Reference(PractitionerRole/generalpractitioner-guthridge-jarred)
|
||||
* note.text = "Itchy and burning eye, foreign body sensation. Mucoid discharge."
|
1
static/uploads/fsh_output/input/input.json
Normal file
1
static/uploads/fsh_output/input/input.json
Normal file
@ -0,0 +1 @@
|
||||
{"resourceType":"Encounter","id":"discharge-1","meta":{"profile":["http://hl7.org.au/fhir/core/StructureDefinition/au-core-encounter"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\"><p class=\"res-header-id\"><b>Generated Narrative: Encounter discharge-1</b></p><a name=\"discharge-1\"> </a><a name=\"hcdischarge-1\"> </a><a name=\"discharge-1-en-AU\"> </a><div style=\"display: inline-block; background-color: #d9e0e7; padding: 6px; margin: 4px; border: 1px solid #8da1b4; border-radius: 5px; line-height: 60%\"><p style=\"margin-bottom: 0px\"/><p style=\"margin-bottom: 0px\">Profile: <a href=\"StructureDefinition-au-core-encounter.html\">AU Core Encounter</a></p></div><p><b>status</b>: Finished</p><p><b>class</b>: <a href=\"http://terminology.hl7.org/6.2.0/CodeSystem-v3-ActCode.html#v3-ActCode-EMER\">ActCode EMER</a>: emergency</p><p><b>subject</b>: <a href=\"Patient-ronny-irvine.html\">Ronny Lawrence Irvine Male, DoB: ( DVA Number:\u00a0QX827261)</a></p><p><b>period</b>: 2023-02-20 06:15:00+1000 --> 2023-02-20 18:19:00+1000</p><h3>Locations</h3><table class=\"grid\"><tr><td style=\"display: none\">-</td><td><b>Location</b></td></tr><tr><td style=\"display: none\">*</td><td><a href=\"Location-murrabit-hospital.html\">Location Murrabit Public Hospital</a></td></tr></table><p><b>serviceProvider</b>: <a href=\"Organization-murrabit-hospital.html\">Organization Murrabit Public Hospital</a></p></div>"},"status":"finished","class":{"system":"http://terminology.hl7.org/CodeSystem/v3-ActCode","code":"EMER","display":"emergency"},"subject":{"reference":"Patient/ronny-irvine"},"period":{"start":"2023-02-20T06:15:00+10:00","end":"2023-02-20T18:19:00+10:00"},"location":[{"location":{"reference":"Location/murrabit-hospital"}}],"serviceProvider":{"reference":"Organization/murrabit-hospital"}}
|
8
static/uploads/fsh_output/sushi-config.yaml
Normal file
8
static/uploads/fsh_output/sushi-config.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
canonical: http://example.org
|
||||
fhirVersion: 4.3.0
|
||||
FSHOnly: true
|
||||
applyExtensionMetadataToRoot: false
|
||||
id: example
|
||||
name: Example
|
||||
dependencies:
|
||||
hl7.fhir.us.core: 6.1.0
|
55
static/uploads/output.fsh
Normal file
55
static/uploads/output.fsh
Normal file
@ -0,0 +1,55 @@
|
||||
Alias: $sct = http://snomed.info/sct
|
||||
Alias: $loinc = http://loinc.org
|
||||
Alias: $ihi-status-1 = https://healthterminologies.gov.au/fhir/CodeSystem/ihi-status-1
|
||||
Alias: $ihi-record-status-1 = https://healthterminologies.gov.au/fhir/CodeSystem/ihi-record-status-1
|
||||
Alias: $v2-0203 = http://terminology.hl7.org/CodeSystem/v2-0203
|
||||
|
||||
Instance: banks-mia-leanne
|
||||
InstanceOf: Patient
|
||||
Usage: #example
|
||||
* meta.profile = "http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient"
|
||||
* extension[0].url = "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity"
|
||||
* extension[=].extension.url = "value"
|
||||
* extension[=].extension.valueCodeableConcept = $sct#446141000124107 "Identifies as female gender"
|
||||
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-pronouns"
|
||||
* extension[=].extension.url = "value"
|
||||
* extension[=].extension.valueCodeableConcept = $loinc#LA29519-8 "she/her/her/hers/herself"
|
||||
* extension[+].url = "http://hl7.org/fhir/StructureDefinition/individual-recordedSexOrGender"
|
||||
* extension[=].extension[0].url = "value"
|
||||
* extension[=].extension[=].valueCodeableConcept = $sct#248152002
|
||||
* extension[=].extension[=].valueCodeableConcept.text = "Female"
|
||||
* extension[=].extension[+].url = "type"
|
||||
* extension[=].extension[=].valueCodeableConcept = $sct#1515311000168102 "Biological sex at birth"
|
||||
* identifier.extension[0].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-status"
|
||||
* identifier.extension[=].valueCoding = $ihi-status-1#active
|
||||
* identifier.extension[+].url = "http://hl7.org.au/fhir/StructureDefinition/ihi-record-status"
|
||||
* identifier.extension[=].valueCoding = $ihi-record-status-1#verified "verified"
|
||||
* identifier.type = $v2-0203#NI
|
||||
* identifier.type.text = "IHI"
|
||||
* identifier.system = "http://ns.electronichealth.net.au/id/hi/ihi/1.0"
|
||||
* identifier.value = "8003608333647261"
|
||||
* name.use = #usual
|
||||
* name.family = "Banks"
|
||||
* name.given[0] = "Mia"
|
||||
* name.given[+] = "Leanne"
|
||||
* telecom[0].system = #phone
|
||||
* telecom[=].value = "0270102724"
|
||||
* telecom[=].use = #work
|
||||
* telecom[+].system = #phone
|
||||
* telecom[=].value = "0491574632"
|
||||
* telecom[=].use = #mobile
|
||||
* telecom[+].system = #phone
|
||||
* telecom[=].value = "0270107520"
|
||||
* telecom[=].use = #home
|
||||
* telecom[+].system = #email
|
||||
* telecom[=].value = "mia.banks@myownpersonaldomain.com"
|
||||
* telecom[+].system = #phone
|
||||
* telecom[=].value = "270107520"
|
||||
* telecom[=].use = #home
|
||||
* gender = #female
|
||||
* birthDate = "1983-08-25"
|
||||
* address.line = "50 Sebastien St"
|
||||
* address.city = "Minjary"
|
||||
* address.state = "NSW"
|
||||
* address.postalCode = "2720"
|
||||
* address.country = "AU"
|
36
supervisord.conf
Normal file
36
supervisord.conf
Normal file
@ -0,0 +1,36 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/app/logs/supervisord.log
|
||||
logfile_maxbytes=50MB
|
||||
logfile_backups=10
|
||||
pidfile=/app/logs/supervisord.pid
|
||||
|
||||
[program:flask]
|
||||
command=/app/venv/bin/python /app/app.py
|
||||
directory=/app
|
||||
environment=FLASK_APP="app.py",FLASK_ENV="development",NODE_PATH="/usr/lib/node_modules"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopwaitsecs=10
|
||||
stdout_logfile=/app/logs/flask.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=5
|
||||
stderr_logfile=/app/logs/flask_err.log
|
||||
stderr_logfile_maxbytes=10MB
|
||||
stderr_logfile_backups=5
|
||||
|
||||
[program:tomcat]
|
||||
command=/usr/local/tomcat/bin/catalina.sh run
|
||||
directory=/usr/local/tomcat
|
||||
environment=SPRING_CONFIG_LOCATION="file:/usr/local/tomcat/conf/application.yaml",NODE_PATH="/usr/lib/node_modules"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=30
|
||||
stopwaitsecs=30
|
||||
stdout_logfile=/app/logs/tomcat.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=5
|
||||
stderr_logfile=/app/logs/tomcat_err.log
|
||||
stderr_logfile_maxbytes=10MB
|
||||
stderr_logfile_backups=5
|
@ -1,20 +1,15 @@
|
||||
{# app/templates/_form_helpers.html #}
|
||||
{% macro render_field(field, label_visible=true) %}
|
||||
<div class="form-group mb-3"> {# Add margin bottom for spacing #}
|
||||
<div class="form-group mb-3">
|
||||
{% if field.type == "BooleanField" %}
|
||||
<div class="form-check">
|
||||
{{ field(class="form-check-input" + (" is-invalid" if field.errors else ""), **kwargs) }}
|
||||
{% if label_visible and field.label %}
|
||||
{{ field.label(class="form-label") }} {# Render label with Bootstrap class #}
|
||||
<label class="form-check-label" for="{{ field.id }}">{{ field.label.text }}</label>
|
||||
{% endif %}
|
||||
|
||||
{# Add is-invalid class if errors exist #}
|
||||
{% set css_class = 'form-control ' + kwargs.pop('class', '') %}
|
||||
{% if field.errors %}
|
||||
{% set css_class = css_class + ' is-invalid' %}
|
||||
{% if field.description %}
|
||||
<small class="form-text text-muted">{{ field.description }}</small>
|
||||
{% endif %}
|
||||
|
||||
{# Render the field itself, passing any extra attributes #}
|
||||
{{ field(class=css_class, **kwargs) }}
|
||||
|
||||
{# Display validation errors #}
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in field.errors %}
|
||||
@ -23,4 +18,25 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if label_visible and field.label %}
|
||||
{{ field.label(class="form-label") }}
|
||||
{% endif %}
|
||||
{% set css_class = 'form-control ' + kwargs.pop('class', '') %}
|
||||
{% if field.errors %}
|
||||
{% set css_class = css_class + ' is-invalid' %}
|
||||
{% endif %}
|
||||
{{ field(class=css_class, **kwargs) }}
|
||||
{% if field.description %}
|
||||
<small class="form-text text-muted">{{ field.description }}</small>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
60
templates/_fsh_output.html
Normal file
60
templates/_fsh_output.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% from "_form_helpers.html" import render_field %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form id="fsh-converter-form" method="POST" enctype="multipart/form-data" class="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field(form.package) }}
|
||||
{{ render_field(form.input_mode) }}
|
||||
<div id="file-upload" style="display: none;">
|
||||
{{ render_field(form.fhir_file) }}
|
||||
</div>
|
||||
<div id="text-input" style="display: none;">
|
||||
{{ render_field(form.fhir_text) }}
|
||||
</div>
|
||||
{{ render_field(form.output_style) }}
|
||||
{{ render_field(form.log_level) }}
|
||||
{{ render_field(form.fhir_version) }}
|
||||
{{ render_field(form.fishing_trip) }}
|
||||
{{ render_field(form.dependencies, placeholder="One per line, e.g., hl7.fhir.us.core@6.1.0") }}
|
||||
{{ render_field(form.indent_rules) }}
|
||||
{{ render_field(form.meta_profile) }}
|
||||
{{ render_field(form.alias_file) }}
|
||||
{{ render_field(form.no_alias) }}
|
||||
<div class="d-grid gap-2 d-sm-flex">
|
||||
{{ form.submit(class="btn btn-success", id="submit-btn") }}
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if error %}
|
||||
<div class="alert alert-danger mt-4">{{ error }}</div>
|
||||
{% endif %}
|
||||
{% if fsh_output %}
|
||||
<div class="alert alert-success mt-4">Conversion successful!</div>
|
||||
<h3 class="mt-4">FSH Output</h3>
|
||||
<pre class="p-3">{{ fsh_output }}</pre>
|
||||
<a href="{{ url_for('download_fsh') }}" class="btn btn-primary">Download FSH</a>
|
||||
{% if comparison_report %}
|
||||
<h3 class="mt-4">Fishing Trip Comparison Report</h3>
|
||||
<a href="{{ url_for('static', filename='uploads/fsh_output/fshing-trip-comparison.html') }}" class="badge bg-primary text-white text-decoration-none mb-3" target="_blank">Click here for SUSHI Validation</a>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if comparison_report.differences %}
|
||||
<p class="text-warning">Differences found in round-trip validation:</p>
|
||||
<ul>
|
||||
{% for diff in comparison_report.differences %}
|
||||
<li>{{ diff.path }}: {{ diff.description }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-success">No differences found in round-trip validation.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
66
templates/about.html
Normal file
66
templates/about.html
Normal file
@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 py-5 my-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="" width="192" height="192">
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">About FHIRFLARE IG Toolkit{% if app_mode == 'lite' %} (Lite Version){% endif %}</h1>
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
A comprehensive toolkit designed for developers and implementers working with FHIR® Implementation Guides (IGs).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<h2>Overview</h2>
|
||||
<p>The FHIRFLARE IG Toolkit is a web application built to simplify the lifecycle of managing, processing, validating, and deploying FHIR Implementation Guides. It provides a central, user-friendly interface to handle common tasks associated with FHIR IGs, streamlining workflows and improving productivity.</p>
|
||||
<p>Whether you're downloading the latest IG versions, checking compliance, converting resources to FHIR Shorthand (FSH), or pushing guides to a test server, this toolkit aims to be an essential companion.</p>
|
||||
{% if app_mode == 'lite' %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>You are currently running the <strong>Lite Version</strong> of the toolkit. This version excludes the built-in HAPI FHIR server and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>You are currently running the <strong>Standalone Version</strong> of the toolkit, which includes a built-in HAPI FHIR server (accessible via the `/fhir` proxy) for local validation and exploration.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h2 class="mt-4">Core Features</h2>
|
||||
<ul>
|
||||
<li><strong>IG Package Management:</strong> Import FHIR IG packages directly from the registry using various version formats (e.g., `1.1.0-preview`, `current`), with flexible dependency handling (Recursive, Patch Canonical, Tree Shaking). View, process, unload, or delete downloaded packages, with detection of duplicate dependencies.</li>
|
||||
<li><strong>IG Processing & Viewing:</strong> Extract and display key information from processed IGs, including defined profiles, referenced resource types, must-support elements, and examples. Visualize profile relationships like `compliesWithProfile` and `imposeProfile`.</li>
|
||||
<li><strong>FHIR Validation:</strong> Validate individual FHIR resources or entire Bundles against the profiles defined within a selected IG. Provides detailed error and warning feedback. (Note: Validation accuracy is still under development, especially for complex constraints).</li>
|
||||
<li><strong>FHIR Server Interaction:</strong>
|
||||
<ul>
|
||||
<li>Push processed IGs (including dependencies) to a target FHIR server with real-time console feedback.</li>
|
||||
<li>Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (GET/POST/PUT/DELETE) and "FHIR UI Operations" pages, supporting both the local HAPI server (in Standalone mode) and external servers.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>FHIR Shorthand (FSH) Conversion:</strong> Convert FHIR JSON or XML resources to FSH using the integrated GoFSH tool. Offers advanced options like context package selection, various output styles, FHIR version selection, dependency loading, alias file usage, and round-trip validation ("Fishing Trip") with SUSHI. Includes a loading indicator during conversion.</li>
|
||||
<li><strong>API Support:</strong> Provides basic API endpoints for programmatic import and push operations.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="mt-4">Technology</h2>
|
||||
<p>The toolkit leverages a combination of technologies:</p>
|
||||
<ul>
|
||||
<li><strong>Backend:</strong> Python with the Flask web framework and SQLAlchemy for database interaction (SQLite).</li>
|
||||
<li><strong>Frontend:</strong> HTML, Bootstrap 5 for styling, and JavaScript for interactivity and dynamic content loading. Uses Lottie-Web for animations.</li>
|
||||
<li><strong>FHIR Tooling:</strong> Integrates GoFSH and SUSHI (via Node.js) for FSH conversion and validation. Utilizes the HAPI FHIR server (in Standalone mode) for robust FHIR validation and operations.</li>
|
||||
<li><strong>Deployment:</strong> Runs within a Docker container managed by Docker Compose and Supervisor, ensuring a consistent environment.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="mt-4">Get Involved</h2>
|
||||
<p>This is an open-source project. Contributions, feedback, and bug reports are welcome!</p>
|
||||
<ul>
|
||||
<li><strong>GitHub Repository:</strong> <a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit" target="_blank" rel="noopener noreferrer">Sudo-JHare/FHIRFLARE-IG-Toolkit</a></li>
|
||||
<li><strong>Report an Issue:</strong> <a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/issues/new/choose" target="_blank" rel="noopener noreferrer">Raise an Issue</a></li>
|
||||
<li><strong>Discussions:</strong> <a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/discussions" target="_blank" rel="noopener noreferrer">Project Discussions</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -5,39 +5,800 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css" rel="stylesheet" />
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<title>{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
|
||||
<title>{% if app_mode == 'lite' %}(Lite Version) {% endif %}{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
|
||||
<style>
|
||||
/* Default (Light Theme) Styles */
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
h1, h5 {
|
||||
font-weight: 600;
|
||||
}
|
||||
.navbar-light {
|
||||
background-color: #ffffff !important;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
color: #007bff !important;
|
||||
}
|
||||
.nav-link {
|
||||
color: #333 !important;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.nav-link:hover {
|
||||
color: #007bff !important;
|
||||
}
|
||||
.nav-link.active {
|
||||
color: #007bff !important;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
.dropdown-menu {
|
||||
list-style: none !important;
|
||||
position: absolute;
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
min-width: 160px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.dropdown-item {
|
||||
padding: 0.5rem 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
.dropdown-item.active {
|
||||
background-color: #007bff;
|
||||
color: white !important;
|
||||
}
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
.text-muted {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
.form-check-input {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
margin-top: 0.25rem;
|
||||
display: inline-block !important;
|
||||
border: 1px solid #6c757d;
|
||||
}
|
||||
.form-check-input:checked {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
.form-check-label {
|
||||
font-size: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.form-check-label i {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.form-check {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.navbar-controls {
|
||||
gap: 0.5rem;
|
||||
padding-right: 0;
|
||||
}
|
||||
/* Footer Styles */
|
||||
footer {
|
||||
background-color: #ffffff;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
display: inline-flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
.footer-left a,
|
||||
.footer-right a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.footer-left a:hover,
|
||||
.footer-right a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
/* Alert Styles */
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border-color: #c3e6cb;
|
||||
}
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
/* --- Unified Button and Badge Styles --- */
|
||||
|
||||
/* Add some basic adjustments for Prism compatibility if needed */
|
||||
/* Ensure pre takes full width and code block behaves */
|
||||
.highlight-code pre, pre[class*="language-"] {
|
||||
margin: 0.5em 0;
|
||||
overflow: auto; /* Allow scrolling */
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none; /* Adjust if theme adds text shadow */
|
||||
white-space: pre-wrap; /* Wrap long lines */
|
||||
word-break: break-all; /* Break long words/tokens */
|
||||
}
|
||||
/* Adjust padding if needed based on theme */
|
||||
:not(pre) > code[class*="language-"], pre[class*="language-"] {
|
||||
background: inherit; /* Ensure background comes from pre or theme */
|
||||
}
|
||||
|
||||
/* General Button Theme (Primary, Secondary) */
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #004085;
|
||||
}
|
||||
.btn-outline-primary {
|
||||
color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
.btn-outline-primary:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
.btn-outline-secondary {
|
||||
color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* --- Prism.js Theme Overrides for Light Mode --- */
|
||||
|
||||
/* Target code blocks only when NOT in dark theme */
|
||||
html:not([data-theme="dark"]) .token.punctuation,
|
||||
html:not([data-theme="dark"]) .token.operator,
|
||||
html:not([data-theme="dark"]) .token.entity,
|
||||
html:not([data-theme="dark"]) .token.url,
|
||||
html:not([data-theme="dark"]) .token.symbol,
|
||||
html:not([data-theme="dark"]) .token.number,
|
||||
html:not([data-theme="dark"]) .token.boolean,
|
||||
html:not([data-theme="dark"]) .token.variable,
|
||||
html:not([data-theme="dark"]) .token.constant,
|
||||
html:not([data-theme="dark"]) .token.property,
|
||||
html:not([data-theme="dark"]) .token.regex,
|
||||
html:not([data-theme="dark"]) .token.inserted,
|
||||
html:not([data-theme="dark"]) .token.null, /* Target null specifically */
|
||||
html:not([data-theme="dark"]) .language-css .token.string,
|
||||
html:not([data-theme="dark"]) .style .token.string,
|
||||
html:not([data-theme="dark"]) .token.important,
|
||||
html:not([data-theme="dark"]) .token.bold {
|
||||
/* Choose a darker color that works on your light code background */
|
||||
/* Example: A dark grey or the default text color */
|
||||
color: #333 !important; /* You might need !important to override theme */
|
||||
}
|
||||
|
||||
/* You might need to adjust other tokens as well based on the specific theme */
|
||||
/* Example: Make strings slightly darker too if needed */
|
||||
html:not([data-theme="dark"]) .token.string {
|
||||
color: #005cc5 !important; /* Example: A shade of blue */
|
||||
}
|
||||
|
||||
/* Ensure background of highlighted code isn't overridden by theme in light mode */
|
||||
html:not([data-theme="dark"]) pre[class*="language-"] {
|
||||
background: #e9ecef !important; /* Match your light theme pre background */
|
||||
}
|
||||
html:not([data-theme="dark"]) :not(pre) > code[class*="language-"] {
|
||||
background: #e9ecef !important; /* Match inline code background */
|
||||
color: #333 !important; /* Ensure inline code text is dark */
|
||||
}
|
||||
|
||||
/* --- End Prism.js Overrides --- */
|
||||
|
||||
/* --- Theme Styles for FSH Code Blocks --- */
|
||||
pre, pre code {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.875em;
|
||||
white-space: pre-wrap; /* Allow wrapping */
|
||||
word-break: break-all; /* Break long lines */
|
||||
}
|
||||
|
||||
/* Target pre blocks specifically used for FSH output if needed */
|
||||
/* If the pre block has a specific class or ID, use that */
|
||||
#fsh-output pre, /* Targets any pre within the output container */
|
||||
._fsh_output pre /* Targets any pre within the partial template content */
|
||||
{
|
||||
background-color: #e9ecef; /* Light theme background */
|
||||
color: #212529; /* Light theme text */
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 600px; /* Adjust as needed */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#fsh-output pre code,
|
||||
._fsh_output pre code {
|
||||
background-color: transparent !important; /* Ensure code tag doesn't override pre bg */
|
||||
padding: 0;
|
||||
color: inherit; /* Inherit text color from pre */
|
||||
font-size: inherit; /* Inherit font size from pre */
|
||||
display: block; /* Make code fill pre */
|
||||
}
|
||||
|
||||
|
||||
/* Dark Theme Override */
|
||||
html[data-theme="dark"] #fsh-output pre,
|
||||
html[data-theme="dark"] ._fsh_output pre
|
||||
{
|
||||
background-color: #495057; /* Dark theme background */
|
||||
color: #f8f9fa; /* Dark theme text */
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #fsh-output pre code,
|
||||
html[data-theme="dark"] ._fsh_output pre code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* --- Theme Styles for FHIR UI Operations (fhir_ui_operations.html) --- */
|
||||
|
||||
/* Operation Block Container */
|
||||
.opblock {
|
||||
border: 1px solid #dee2e6; /* Light theme border */
|
||||
background: #ffffff; /* Light theme background */
|
||||
color: #212529; /* Light theme text */
|
||||
}
|
||||
html[data-theme="dark"] .opblock {
|
||||
border-color: #495057; /* Dark theme border */
|
||||
background: #343a40; /* Dark theme background (card bg) */
|
||||
color: #f8f9fa; /* Dark theme text */
|
||||
}
|
||||
|
||||
/* Operation Summary (Header) */
|
||||
.opblock-summary {
|
||||
background: #f8f9fa; /* Light theme summary bg */
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
color: #212529; /* Default text color for summary */
|
||||
}
|
||||
.opblock-summary:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
html[data-theme="dark"] .opblock-summary {
|
||||
background: #40464c; /* Darker summary bg */
|
||||
border-bottom-color: #495057;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .opblock-summary:hover {
|
||||
background: #495057;
|
||||
}
|
||||
/* Keep method badge colors fixed - DO NOT override .opblock-summary-method */
|
||||
.opblock-summary-description {
|
||||
color: #495057; /* Slightly muted description text */
|
||||
}
|
||||
html[data-theme="dark"] .opblock-summary-description {
|
||||
color: #adb5bd; /* Lighter muted text for dark */
|
||||
}
|
||||
|
||||
/* Operation Body (Expanded Section) */
|
||||
.opblock-body {
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
html[data-theme="dark"] .opblock-body {
|
||||
border-top-color: #495057;
|
||||
}
|
||||
|
||||
/* Section Headers within Body */
|
||||
.opblock-section-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
color: inherit; /* Inherit from .opblock */
|
||||
}
|
||||
html[data-theme="dark"] .opblock-section-header {
|
||||
border-bottom-color: #495057;
|
||||
}
|
||||
.opblock-title {
|
||||
color: inherit; /* Inherit from .opblock */
|
||||
}
|
||||
|
||||
/* Parameters Table */
|
||||
.parameters-table th,
|
||||
.parameters-table td {
|
||||
border: 1px solid #dee2e6;
|
||||
color: inherit; /* Inherit from .opblock */
|
||||
}
|
||||
html[data-theme="dark"] .parameters-table th,
|
||||
html[data-theme="dark"] .parameters-table td {
|
||||
border-color: #495057;
|
||||
}
|
||||
.parameter__type,
|
||||
.parameter__in {
|
||||
color: #6c757d; /* Use standard muted color */
|
||||
}
|
||||
html[data-theme="dark"] .parameter__type,
|
||||
html[data-theme="dark"] .parameter__in {
|
||||
color: #adb5bd; /* Use standard dark muted color */
|
||||
}
|
||||
.parameters-col_description input[type="text"],
|
||||
.parameters-col_description input[type="number"] {
|
||||
background-color: #fff; /* Match light form controls */
|
||||
border: 1px solid #ced4da;
|
||||
color: #212529;
|
||||
}
|
||||
html[data-theme="dark"] .parameters-col_description input[type="text"],
|
||||
html[data-theme="dark"] .parameters-col_description input[type="number"] {
|
||||
background-color: #495057; /* Match dark form controls */
|
||||
border-color: #6c757d;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Request Body Textarea */
|
||||
.request-body-textarea {
|
||||
background-color: #fff; /* Match light form controls */
|
||||
border: 1px solid #ced4da;
|
||||
color: #212529;
|
||||
}
|
||||
html[data-theme="dark"] .request-body-textarea {
|
||||
background-color: #495057; /* Match dark form controls */
|
||||
border-color: #6c757d;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Responses Table */
|
||||
.responses-table th,
|
||||
.responses-table td {
|
||||
border: 1px solid #dee2e6;
|
||||
color: inherit;
|
||||
}
|
||||
html[data-theme="dark"] .responses-table th,
|
||||
html[data-theme="dark"] .responses-table td {
|
||||
border-color: #495057;
|
||||
}
|
||||
|
||||
/* Example/Schema Tabs & Controls */
|
||||
.response-control-media-type {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .response-control-media-type {
|
||||
background-color: #40464c; /* Match summary header bg */
|
||||
}
|
||||
.tablinks.badge { /* Default inactive tab */
|
||||
background: #dee2e6 !important; /* Light grey inactive */
|
||||
color: #333 !important;
|
||||
}
|
||||
.tablinks.badge.active { /* Default active tab */
|
||||
background: #007bff !important; /* Use primary color */
|
||||
color: white !important;
|
||||
}
|
||||
html[data-theme="dark"] .tablinks.badge { /* Dark inactive tab */
|
||||
background: #6c757d !important; /* Dark grey inactive */
|
||||
color: white !important;
|
||||
}
|
||||
html[data-theme="dark"] .tablinks.badge.active { /* Dark active tab */
|
||||
background: #4dabf7 !important; /* Use dark primary color */
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* Code/Output Blocks */
|
||||
.highlight-code,
|
||||
.request-url-output,
|
||||
.curl-output,
|
||||
.execute-wrapper pre.response-output-content {
|
||||
background: #e9ecef; /* Light theme pre background */
|
||||
color: #212529;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
html[data-theme="dark"] .highlight-code,
|
||||
html[data-theme="dark"] .request-url-output,
|
||||
html[data-theme="dark"] .curl-output,
|
||||
html[data-theme="dark"] .execute-wrapper pre.response-output-content {
|
||||
background: #495057; /* Dark theme pre background */
|
||||
color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
/* Narrative Response Box */
|
||||
.execute-wrapper div.response-output-narrative {
|
||||
background: #ffffff;
|
||||
color: #212529;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
html[data-theme="dark"] .execute-wrapper div.response-output-narrative {
|
||||
background: #343a40; /* Match card body */
|
||||
color: #f8f9fa;
|
||||
border: 1px solid #495057;
|
||||
}
|
||||
|
||||
/* Response Status Text */
|
||||
.execute-wrapper .response-status[style*="color: green"] { color: #198754 !important; }
|
||||
.execute-wrapper .response-status[style*="color: red"] { color: #dc3545 !important; }
|
||||
html[data-theme="dark"] .execute-wrapper .response-status[style*="color: green"] { color: #20c997 !important; }
|
||||
html[data-theme="dark"] .execute-wrapper .response-status[style*="color: red"] { color: #e63946 !important; }
|
||||
|
||||
|
||||
/* Specific Action Buttons (Success, Danger, Warning, Info) */
|
||||
.btn-success { background-color: #198754; border-color: #198754; color: white; }
|
||||
.btn-success:hover { background-color: #157347; border-color: #146c43; }
|
||||
.btn-outline-success { color: #198754; border-color: #198754; }
|
||||
.btn-outline-success:hover { background-color: #198754; color: white; }
|
||||
|
||||
.btn-danger { background-color: #dc3545; border-color: #dc3545; color: white; }
|
||||
.btn-danger:hover { background-color: #bb2d3b; border-color: #b02a37; }
|
||||
.btn-outline-danger { color: #dc3545; border-color: #dc3545; }
|
||||
.btn-outline-danger:hover { background-color: #dc3545; color: white; }
|
||||
|
||||
.btn-warning { background-color: #ffc107; border-color: #ffc107; color: #000; }
|
||||
.btn-warning:hover { background-color: #ffca2c; border-color: #ffc720; }
|
||||
.btn-outline-warning { color: #ffc107; border-color: #ffc107; }
|
||||
.btn-outline-warning:hover { background-color: #ffc107; color: #000; }
|
||||
|
||||
.btn-info { background-color: #0dcaf0; border-color: #0dcaf0; color: #000; }
|
||||
.btn-info:hover { background-color: #31d2f2; border-color: #25cff2; }
|
||||
.btn-outline-info { color: #0dcaf0; border-color: #0dcaf0; }
|
||||
.btn-outline-info:hover { background-color: #0dcaf0; color: #000; }
|
||||
|
||||
/* General Badge Theme */
|
||||
.badge {
|
||||
padding: 0.4em 0.6em; /* Slightly larger padding */
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* Specific Badge Backgrounds (Add more as needed) */
|
||||
.badge.bg-primary { background-color: #007bff !important; color: white !important; }
|
||||
.badge.bg-secondary { background-color: #6c757d !important; color: white !important; }
|
||||
.badge.bg-success { background-color: #198754 !important; color: white !important; }
|
||||
.badge.bg-danger { background-color: #dc3545 !important; color: white !important; }
|
||||
.badge.bg-warning { background-color: #ffc107 !important; color: #000 !important; }
|
||||
.badge.bg-info { background-color: #0dcaf0 !important; color: #000 !important; }
|
||||
.badge.bg-light { background-color: #f8f9fa !important; color: #000 !important; border: 1px solid #dee2e6; }
|
||||
.badge.bg-dark { background-color: #343a40 !important; color: white !important; }
|
||||
|
||||
/* Copy Button Specific Style */
|
||||
.btn-copy { /* Optional: Add a specific class if needed, or style .btn-outline-secondary directly */
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* --- Dark Theme Overrides --- */
|
||||
html[data-theme="dark"] .btn-primary { background-color: #4dabf7; border-color: #4dabf7; color: #000; }
|
||||
html[data-theme="dark"] .btn-primary:hover { background-color: #68bcef; border-color: #5fb7ea; }
|
||||
html[data-theme="dark"] .btn-outline-primary { color: #4dabf7; border-color: #4dabf7; }
|
||||
html[data-theme="dark"] .btn-outline-primary:hover { background-color: #4dabf7; color: #000; }
|
||||
|
||||
html[data-theme="dark"] .btn-secondary { background-color: #6c757d; border-color: #6c757d; color: white; }
|
||||
html[data-theme="dark"] .btn-secondary:hover { background-color: #5a6268; border-color: #545b62; }
|
||||
html[data-theme="dark"] .btn-outline-secondary { color: #adb5bd; border-color: #6c757d; } /* Lighter text/border */
|
||||
html[data-theme="dark"] .btn-outline-secondary:hover { background-color: #6c757d; color: white; }
|
||||
|
||||
html[data-theme="dark"] .btn-success { background-color: #20c997; border-color: #20c997; color: #000; }
|
||||
html[data-theme="dark"] .btn-success:hover { background-color: #3ce0ac; border-color: #36d8a3; }
|
||||
html[data-theme="dark"] .btn-outline-success { color: #20c997; border-color: #20c997; }
|
||||
html[data-theme="dark"] .btn-outline-success:hover { background-color: #20c997; color: #000; }
|
||||
|
||||
/* Keep danger as is */
|
||||
html[data-theme="dark"] .btn-danger { background-color: #e63946; border-color: #e63946; color: white; }
|
||||
html[data-theme="dark"] .btn-danger:hover { background-color: #c82b39; border-color: #bd2130; }
|
||||
html[data-theme="dark"] .btn-outline-danger { color: #e63946; border-color: #e63946; }
|
||||
html[data-theme="dark"] .btn-outline-danger:hover { background-color: #e63946; color: white; }
|
||||
|
||||
html[data-theme="dark"] .btn-warning { background-color: #ffca2c; border-color: #ffca2c; color: #000; }
|
||||
html[data-theme="dark"] .btn-warning:hover { background-color: #ffd24a; border-color: #ffce3d; }
|
||||
html[data-theme="dark"] .btn-outline-warning { color: #ffca2c; border-color: #ffca2c; }
|
||||
html[data-theme="dark"] .btn-outline-warning:hover { background-color: #ffca2c; color: #000; }
|
||||
|
||||
html[data-theme="dark"] .btn-info { background-color: #22b8cf; border-color: #22b8cf; color: #000; }
|
||||
html[data-theme="dark"] .btn-info:hover { background-color: #44cee3; border-color: #38cae0; }
|
||||
html[data-theme="dark"] .btn-outline-info { color: #22b8cf; border-color: #22b8cf; }
|
||||
html[data-theme="dark"] .btn-outline-info:hover { background-color: #22b8cf; color: #000; }
|
||||
|
||||
/* Dark Theme Badges */
|
||||
html[data-theme="dark"] .badge.bg-primary { background-color: #4dabf7 !important; color: #000 !important; }
|
||||
html[data-theme="dark"] .badge.bg-secondary { background-color: #6c757d !important; color: white !important; }
|
||||
html[data-theme="dark"] .badge.bg-success { background-color: #20c997 !important; color: #000 !important; }
|
||||
html[data-theme="dark"] .badge.bg-danger { background-color: #e63946 !important; color: white !important; }
|
||||
html[data-theme="dark"] .badge.bg-warning { background-color: #ffca2c !important; color: #000 !important; }
|
||||
html[data-theme="dark"] .badge.bg-info { background-color: #22b8cf !important; color: #000 !important; }
|
||||
html[data-theme="dark"] .badge.bg-light { background-color: #495057 !important; color: #f8f9fa !important; border: 1px solid #6c757d; }
|
||||
html[data-theme="dark"] .badge.bg-dark { background-color: #adb5bd !important; color: #000 !important; }
|
||||
|
||||
/* --- Themed Backgrounds for Code/Structure View (cp_view_processed_ig.html) --- */
|
||||
|
||||
/* Styling for <pre> blocks containing code */
|
||||
#raw-structure-wrapper pre,
|
||||
#example-content-wrapper pre {
|
||||
background-color: #e9ecef; /* Light theme background */
|
||||
color: #212529; /* Light theme text */
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 400px; /* Keep existing max-height */
|
||||
overflow-y: auto; /* Keep existing overflow */
|
||||
white-space: pre-wrap; /* Ensure wrapping */
|
||||
word-break: break-all; /* Break long strings */
|
||||
}
|
||||
|
||||
/* Ensure code tag inside inherits background and has appropriate font */
|
||||
#raw-structure-wrapper pre code,
|
||||
#example-content-wrapper pre code {
|
||||
background-color: transparent !important; /* Let pre handle background */
|
||||
padding: 0;
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.875em;
|
||||
color: inherit; /* Inherit text color from pre */
|
||||
display: block; /* Make code block fill pre */
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Styling for the Structure Definition tree list items */
|
||||
.structure-tree-root .list-group-item {
|
||||
background-color: #ffffff; /* Light theme default background */
|
||||
border-color: #dee2e6;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.structure-tree-root .list-group-item-warning {
|
||||
background-color: #fff3cd; /* Bootstrap light theme warning */
|
||||
border-color: #ffeeba;
|
||||
/*color: #856404; Ensure text is readable */
|
||||
}
|
||||
|
||||
/* --- Dark Theme Overrides for Code/Structure View --- */
|
||||
html[data-theme="dark"] #raw-structure-wrapper pre,
|
||||
html[data-theme="dark"] #example-content-wrapper pre {
|
||||
background-color: #495057; /* Dark theme background */
|
||||
color: #f8f9fa; /* Dark theme text */
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
/* Dark theme code text color already inherited, but explicit doesn't hurt */
|
||||
html[data-theme="dark"] #raw-structure-wrapper pre code,
|
||||
html[data-theme="dark"] #example-content-wrapper pre code {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Dark theme structure list items */
|
||||
html[data-theme="dark"] .structure-tree-root .list-group-item {
|
||||
background-color: #343a40; /* Dark theme card background */
|
||||
border-color: #495057;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .structure-tree-root .list-group-item-warning {
|
||||
background-color: #664d03; /* Darker warning background */
|
||||
border-color: #997404;
|
||||
/*color: #ffecb5; Lighter text for contrast */
|
||||
}
|
||||
|
||||
/* Dark Theme Styles */
|
||||
html[data-theme="dark"] body {
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .navbar-light {
|
||||
background-color: #343a40 !important;
|
||||
border-bottom: 1px solid #495057;
|
||||
}
|
||||
html[data-theme="dark"] .navbar-brand {
|
||||
color: #4dabf7 !important;
|
||||
}
|
||||
html[data-theme="dark"] .nav-link {
|
||||
color: #f8f9fa !important;
|
||||
}
|
||||
html[data-theme="dark"] .nav-link:hover,
|
||||
html[data-theme="dark"] .nav-link.active {
|
||||
color: #4dabf7 !important;
|
||||
border-bottom-color: #4dabf7;
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-menu {
|
||||
background-color: #495057;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-item {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-item:hover {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-item.active {
|
||||
background-color: #4dabf7;
|
||||
color: white !important;
|
||||
}
|
||||
html[data-theme="dark"] .card {
|
||||
background-color: #343a40;
|
||||
border: 1px solid #495057;
|
||||
}
|
||||
html[data-theme="dark"] .text-muted {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
html[data-theme="dark"] h1,
|
||||
html[data-theme="dark"] h5 {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .form-label {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .form-control {
|
||||
background-color: #495057;
|
||||
color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
html[data-theme="dark"] .form-control::placeholder {
|
||||
color: #adb5bd;
|
||||
}
|
||||
html[data-theme="dark"] .form-select {
|
||||
background-color: #495057;
|
||||
color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
html[data-theme="dark"] .alert-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
html[data-theme="dark"] .alert-success {
|
||||
background-color: #198754;
|
||||
color: white;
|
||||
border-color: #198754;
|
||||
}
|
||||
html[data-theme="dark"] .alert-info {
|
||||
background-color: #0dcaf0;
|
||||
color: white;
|
||||
border-color: #0dcaf0;
|
||||
}
|
||||
html[data-theme="dark"] .form-check-input {
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
html[data-theme="dark"] .form-check-input:checked {
|
||||
background-color: #4dabf7;
|
||||
border-color: #4dabf7;
|
||||
}
|
||||
html[data-theme="dark"] footer {
|
||||
background-color: #343a40;
|
||||
border-top: 1px solid #495057;
|
||||
}
|
||||
html[data-theme="dark"] .footer-left a,
|
||||
html[data-theme="dark"] .footer-right a {
|
||||
color: #4dabf7;
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.navbar-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
.navbar-controls .form-check {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
footer {
|
||||
height: auto;
|
||||
padding: 0.5rem;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.footer-left a,
|
||||
.footer-right a {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}</a>
|
||||
<a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}{% if app_mode == 'lite' %} (Lite){% endif %}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<div class="collapse navbar-collapse d-flex justify-content-between" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'index' else '' }}" aria-current="page" href="{{ url_for('index') }}"><i class="bi bi-house-door me-1"></i> Home</a>
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'index' else '' }}" aria-current="page" href="{{ url_for('index') }}"><i class="fas fa-house me-1"></i> Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="bi bi-journal-arrow-down me-1"></i> Manage FHIR Packages</a>
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="fas fa-folder-open me-1"></i> Manage FHIR Packages</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="bi bi-download me-1"></i> Import IGs</a>
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="fas fa-download me-1"></i> Import IGs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="bi bi-upload me-1"></i> Push IGs</a>
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="fas fa-upload me-1"></i> Push IGs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'validate_sample' else '' }}" href="{{ url_for('validate_sample') }}"><i class="bi bi-check-circle me-1"></i> Validate FHIR Sample</a>
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'validate_sample' else '' }}" href="{{ url_for('validate_sample') }}"><i class="fas fa-check-circle me-1"></i> Validate FHIR Sample</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'fhir_ui' else '' }}" href="{{ url_for('fhir_ui') }}"><i class="fas fa-cloud-download-alt me-1"></i> FHIR API Explorer</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'fhir_ui_operations' else '' }}" href="{{ url_for('fhir_ui_operations') }}"><i class="bi bi-gear me-1"></i> FHIR UI Operations</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'fsh_converter' else '' }}" href="{{ url_for('fsh_converter') }}"><i class="fas fa-file-code me-1"></i> FSH Converter</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-controls d-flex align-items-center">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="themeToggle" onchange="toggleTheme()" aria-label="Toggle dark mode" {% if request.cookies.get('theme') == 'dark' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="themeToggle">
|
||||
<i class="fas fa-sun"></i>
|
||||
<i class="fas fa-moon d-none"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container flex-grow-1">
|
||||
<main class="flex-grow-1">
|
||||
<div class="container mt-4">
|
||||
<!-- Flashed Messages Section -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
@ -48,11 +809,10 @@
|
||||
'alert-success' if category == 'success' else
|
||||
'alert-info' }}
|
||||
alert-dismissible fade show" role="alert">
|
||||
<!-- Add an icon based on the category -->
|
||||
<i class="bi
|
||||
{{ 'bi-exclamation-triangle-fill me-2' if category == 'error' else
|
||||
'bi-check-circle-fill me-2' if category == 'success' else
|
||||
'bi-info-circle-fill me-2' }}"></i>
|
||||
<i class="fas
|
||||
{{ 'fa-exclamation-triangle me-2' if category == 'error' else
|
||||
'fa-check-circle me-2' if category == 'success' else
|
||||
'fa-info-circle me-2' }}"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@ -61,24 +821,70 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer mt-auto py-3 bg-light">
|
||||
<div class="container text-center">
|
||||
{% set current_year = now.year %}
|
||||
<span class="text-muted">© {{ current_year }} {{ site_name }}. All rights reserved.</span>
|
||||
<footer class="main-footer" role="contentinfo">
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="footer-left">
|
||||
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit" target="_blank" rel="noreferrer" aria-label="Project Github">Project Github</a>
|
||||
<a href="/about" aria-label="Learn about this project">What is FHIRFLARE</a>
|
||||
<a href="https://www.hl7.org/fhir/" target="_blank" rel="noreferrer" aria-label="Visit FHIR website">FHIR</a>
|
||||
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/issues/new/choose" class="text-danger text-decoration-none" aria-label="FHIRFLARE support"><i class="fas fa-exclamation-circle me-1"></i> Raise an Issue</a>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/discussions" target="_blank" rel="noreferrer" aria-label="Project Discussion">Project Discussions</a>
|
||||
<a href="https://github.com/Sudo-JHare" aria-label="Developer">Developer</a>
|
||||
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/blob/main/LICENSE.md" aria-label="License">License</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) })
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const toggle = document.getElementById('themeToggle');
|
||||
const sunIcon = document.querySelector('.fa-sun');
|
||||
const moonIcon = document.querySelector('.fa-moon');
|
||||
if (toggle.checked) {
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
sunIcon.classList.add('d-none');
|
||||
moonIcon.classList.remove('d-none');
|
||||
} else {
|
||||
html.removeAttribute('data-theme');
|
||||
localStorage.setItem('theme', 'light');
|
||||
sunIcon.classList.remove('d-none');
|
||||
moonIcon.classList.add('d-none');
|
||||
}
|
||||
if (typeof loadLottieAnimation === 'function') {
|
||||
const newTheme = toggle.checked ? 'dark' : 'light';
|
||||
loadLottieAnimation(newTheme);
|
||||
}
|
||||
// Set cookie to persist theme
|
||||
document.cookie = `theme=${toggle.checked ? 'dark' : 'light'}; path=/; max-age=31536000`;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const savedTheme = localStorage.getItem('theme') || (document.cookie.includes('theme=dark') ? 'dark' : 'light');
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const sunIcon = document.querySelector('.fa-sun');
|
||||
const moonIcon = document.querySelector('.fa-moon');
|
||||
if (savedTheme === 'dark' && themeToggle) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
themeToggle.checked = true;
|
||||
sunIcon.classList.add('d-none');
|
||||
moonIcon.classList.remove('d-none');
|
||||
}
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach(t => new bootstrap.Tooltip(t));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
@ -8,11 +8,13 @@
|
||||
<p class="lead mb-4">
|
||||
This is the starting Point for your Journey through the IG's
|
||||
</p>
|
||||
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 disabled">Manage FHIR Packages</a>
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
|
||||
</div>
|
||||
------------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
300
templates/fhir_ui.html
Normal file
300
templates/fhir_ui.html
Normal file
@ -0,0 +1,300 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 py-5 my-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192">
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">FHIR API Explorer</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
Interact with FHIR servers using GET, POST, PUT, or DELETE requests. Toggle between local HAPI or a custom server to explore resources or perform searches.
|
||||
</p>
|
||||
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
||||
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
|
||||
<a href="{{ url_for('fhir_ui_operations') }}" class="btn btn-outline-secondary btn-lg px-4">FHIR UI Operations</a>
|
||||
</div>
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-cloud-arrow-down me-2"></i>Send FHIR Request</div>
|
||||
<div class="card-body">
|
||||
<form id="fhirRequestForm" onsubmit="return false;">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">FHIR Server</label>
|
||||
<div class="input-group">
|
||||
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
||||
<span id="toggleLabel">Use Local HAPI</span>
|
||||
</button>
|
||||
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
|
||||
</div>
|
||||
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (http://localhost:8080/fhir) or enter a custom FHIR server URL.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="fhirPath" class="form-label">FHIR Path</label>
|
||||
<input type="text" class="form-control" id="fhirPath" name="fhir_path" placeholder="e.g., Patient/wang-li" required aria-describedby="fhirPathHelp">
|
||||
<small id="fhirPathHelp" class="form-text text-muted">Enter a resource path (e.g., Patient, Observation/example) or '_search' for search queries.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Request Type</label>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<input type="radio" class="btn-check" name="method" id="get" value="GET" checked>
|
||||
<label class="btn btn-outline-success" for="get"><span class="badge bg-success">GET</span></label>
|
||||
|
||||
<input type="radio" class="btn-check" name="method" id="post" value="POST">
|
||||
<label class="btn btn-outline-primary" for="post"><span class="badge bg-primary">POST</span></label>
|
||||
|
||||
<input type="radio" class="btn-check" name="method" id="put" value="PUT">
|
||||
<label class="btn btn-outline-warning" for="put"><span class="badge bg-warning text-dark">PUT</span></label>
|
||||
|
||||
<input type="radio" class="btn-check" name="method" id="delete" value="DELETE">
|
||||
<label class="btn btn-outline-danger" for="delete"><span class="badge bg-danger">DELETE</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" id="requestBodyGroup" style="display: none;">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<label for="requestBody" class="form-label me-2 mb-0">Request Body</label>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm btn-copy" id="copyRequestBody" title="Copy to Clipboard"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<textarea class="form-control" id="requestBody" name="request_body" rows="6" placeholder="For POST: JSON resource (e.g., {'resourceType': 'Patient', ...}) or search params (e.g., name=John&birthdate=gt2000)"></textarea>
|
||||
<div id="jsonError" class="text-danger mt-1" style="display: none;"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="sendRequest">Send Request</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4" id="responseCard" style="display: none;">
|
||||
<div class="card-header">Response</div>
|
||||
<div class="card-body">
|
||||
<h5>Status: <span id="responseStatus" class="badge"></span></h5>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<h6 class="me-2 mb-0">Headers</h6>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm btn-copy" id="copyResponseHeaders" title="Copy to Clipboard"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<pre id="responseHeaders" class="border p-2 bg-light mb-3" style="max-height: 200px; overflow-y: auto;"></pre>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<h6 class="me-2 mb-0">Body</h6>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm btn-copy" id="copyResponseBody" title="Copy to Clipboard"><i class="bi bi-clipboard"></i></button>
|
||||
</div>
|
||||
<pre id="responseBody" class="border p-2 bg-light" style="max-height: 400px; overflow-y: auto;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log("DOMContentLoaded event fired."); // Log when the listener starts
|
||||
|
||||
// --- Get DOM Elements & Check Existence ---
|
||||
const form = document.getElementById('fhirRequestForm');
|
||||
const sendButton = document.getElementById('sendRequest');
|
||||
const fhirPath = document.getElementById('fhirPath');
|
||||
const requestBody = document.getElementById('requestBody');
|
||||
const requestBodyGroup = document.getElementById('requestBodyGroup');
|
||||
const jsonError = document.getElementById('jsonError');
|
||||
const responseCard = document.getElementById('responseCard');
|
||||
const responseStatus = document.getElementById('responseStatus');
|
||||
const responseHeaders = document.getElementById('responseHeaders');
|
||||
const responseBody = document.getElementById('responseBody');
|
||||
const methodRadios = document.getElementsByName('method');
|
||||
const toggleServerButton = document.getElementById('toggleServer');
|
||||
const toggleLabel = document.getElementById('toggleLabel');
|
||||
const fhirServerUrl = document.getElementById('fhirServerUrl');
|
||||
const copyRequestBodyButton = document.getElementById('copyRequestBody');
|
||||
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
|
||||
const copyResponseBodyButton = document.getElementById('copyResponseBody');
|
||||
|
||||
// --- Element Existence Check ---
|
||||
let elementsReady = true;
|
||||
if (!form) { console.error("Element not found: #fhirRequestForm"); elementsReady = false; }
|
||||
if (!sendButton) { console.error("Element not found: #sendRequest"); elementsReady = false; }
|
||||
if (!fhirPath) { console.error("Element not found: #fhirPath"); elementsReady = false; }
|
||||
if (!requestBody) { console.warn("Element not found: #requestBody"); } // Warn only, might be ok if not POST/PUT
|
||||
if (!requestBodyGroup) { console.warn("Element not found: #requestBodyGroup"); } // Warn only
|
||||
if (!jsonError) { console.warn("Element not found: #jsonError"); } // Warn only
|
||||
if (!responseCard) { console.error("Element not found: #responseCard"); elementsReady = false; }
|
||||
if (!responseStatus) { console.error("Element not found: #responseStatus"); elementsReady = false; }
|
||||
if (!responseHeaders) { console.error("Element not found: #responseHeaders"); elementsReady = false; }
|
||||
if (!responseBody) { console.error("Element not found: #responseBody"); elementsReady = false; }
|
||||
if (!toggleServerButton) { console.error("Element not found: #toggleServer"); elementsReady = false; }
|
||||
if (!toggleLabel) { console.error("Element not found: #toggleLabel"); elementsReady = false; }
|
||||
if (!fhirServerUrl) { console.error("Element not found: #fhirServerUrl"); elementsReady = false; }
|
||||
// Add checks for copy buttons if needed
|
||||
|
||||
if (!elementsReady) {
|
||||
console.error("One or more critical UI elements could not be found. Script execution halted.");
|
||||
// Optionally display a user-facing error message here
|
||||
// const errorDisplay = document.getElementById('some-error-area');
|
||||
// if(errorDisplay) errorDisplay.textContent = "Error initializing UI components.";
|
||||
return; // Stop script execution if critical elements are missing
|
||||
}
|
||||
console.log("All critical elements checked/found.");
|
||||
|
||||
// --- State Variable ---
|
||||
let useLocalHapi = true; // Default state, will be adjusted by Lite mode check
|
||||
|
||||
// --- DEFINE FUNCTIONS (as before) ---
|
||||
|
||||
function updateServerToggleUI() {
|
||||
// Add checks inside the function too, just in case
|
||||
if (!toggleLabel || !fhirServerUrl) {
|
||||
console.error("updateServerToggleUI: Toggle UI elements became unavailable!");
|
||||
return;
|
||||
}
|
||||
toggleLabel.textContent = useLocalHapi ? 'Use Local HAPI' : 'Use Custom URL';
|
||||
fhirServerUrl.style.display = useLocalHapi ? 'none' : 'block';
|
||||
fhirServerUrl.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
function toggleServer() {
|
||||
if (appMode === 'lite') return; // Ignore clicks in Lite mode
|
||||
useLocalHapi = !useLocalHapi;
|
||||
updateServerToggleUI();
|
||||
if (useLocalHapi && fhirServerUrl) {
|
||||
fhirServerUrl.value = '';
|
||||
}
|
||||
console.log(`Server toggled: ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
|
||||
}
|
||||
|
||||
function updateRequestBodyVisibility() {
|
||||
const selectedMethod = document.querySelector('input[name="method"]:checked')?.value;
|
||||
if (!requestBodyGroup || !selectedMethod) return;
|
||||
requestBodyGroup.style.display = (selectedMethod === 'POST' || selectedMethod === 'PUT') ? 'block' : 'none';
|
||||
if (selectedMethod !== 'POST' && selectedMethod !== 'PUT') {
|
||||
if (requestBody) requestBody.value = '';
|
||||
if (jsonError) jsonError.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function validateRequestBody(method, path) {
|
||||
// Added check for requestBody existence
|
||||
if (!requestBody || !jsonError) return method === 'GET' ? null : '';
|
||||
if (!requestBody.value.trim()) {
|
||||
requestBody.classList.remove('is-invalid');
|
||||
jsonError.style.display = 'none';
|
||||
return method === 'GET' ? null : '';
|
||||
}
|
||||
const isSearch = path && path.endsWith('_search');
|
||||
if (method === 'POST' && isSearch) { return requestBody.value; }
|
||||
try {
|
||||
JSON.parse(requestBody.value);
|
||||
requestBody.classList.remove('is-invalid');
|
||||
jsonError.style.display = 'none';
|
||||
return requestBody.value;
|
||||
} catch (e) {
|
||||
requestBody.classList.add('is-invalid');
|
||||
jsonError.textContent = `Invalid JSON: ${e.message}`;
|
||||
jsonError.style.display = 'block';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanFhirPath(path) {
|
||||
if (!path) return '';
|
||||
const cleaned = path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
async function copyToClipboard(text, button) {
|
||||
if (!text || !button) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
const originalIcon = button.innerHTML;
|
||||
button.innerHTML = '<i class="bi bi-check-lg"></i>';
|
||||
setTimeout(() => { button.innerHTML = originalIcon; }, 2000);
|
||||
} catch (err) { console.error('Copy failed:', err); alert('Failed to copy to clipboard'); }
|
||||
}
|
||||
|
||||
// --- INITIAL SETUP & MODE CHECK ---
|
||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
||||
console.log('App Mode:', appMode);
|
||||
|
||||
if (appMode === 'lite') {
|
||||
console.log('Lite mode detected: Disabling local HAPI toggle and setting default.');
|
||||
// Check toggleServerButton again before using
|
||||
if (toggleServerButton) {
|
||||
toggleServerButton.disabled = true;
|
||||
toggleServerButton.title = "Local HAPI is not available in Lite mode";
|
||||
toggleServerButton.classList.add('disabled');
|
||||
useLocalHapi = false;
|
||||
if (fhirServerUrl) {
|
||||
fhirServerUrl.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
|
||||
}
|
||||
} else {
|
||||
// This console log should have appeared earlier if button was missing
|
||||
console.error("Lite mode: Could not find toggle server button (#toggleServer) to disable.");
|
||||
}
|
||||
} else {
|
||||
console.log('Standalone mode detected: Enabling local HAPI toggle.');
|
||||
if (toggleServerButton) {
|
||||
toggleServerButton.disabled = false;
|
||||
toggleServerButton.title = "";
|
||||
toggleServerButton.classList.remove('disabled');
|
||||
} else {
|
||||
console.error("Standalone mode: Could not find toggle server button (#toggleServer) to enable.");
|
||||
}
|
||||
}
|
||||
|
||||
// --- SET INITIAL UI STATE ---
|
||||
// Call these *after* the mode check and function definitions
|
||||
updateServerToggleUI();
|
||||
updateRequestBodyVisibility();
|
||||
|
||||
// --- ATTACH EVENT LISTENERS ---
|
||||
|
||||
// Server Toggle Button
|
||||
if (toggleServerButton) { // Check again before adding listener
|
||||
toggleServerButton.addEventListener('click', toggleServer);
|
||||
} else {
|
||||
console.error("Cannot attach listener: Toggle Server Button not found.");
|
||||
}
|
||||
|
||||
// Copy Buttons (Add checks)
|
||||
if (copyRequestBodyButton && requestBody) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBody.value, copyRequestBodyButton)); }
|
||||
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
|
||||
if (copyResponseBodyButton && responseBody) { copyResponseBodyButton.addEventListener('click', () => copyToClipboard(responseBody.textContent, copyResponseBodyButton)); }
|
||||
|
||||
// Method Radio Buttons
|
||||
if (methodRadios) {
|
||||
methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); });
|
||||
}
|
||||
|
||||
// Request Body Input Validation
|
||||
if (requestBody && fhirPath) {
|
||||
requestBody.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPath.value ));
|
||||
}
|
||||
|
||||
// Send Request Button
|
||||
if (sendButton && fhirPath && form) { // Added form check for CSRF
|
||||
sendButton.addEventListener('click', async function() {
|
||||
// (Keep the existing fetch/send logic here)
|
||||
// ...
|
||||
// Ensure you check for element existence inside this handler too if needed, e.g.:
|
||||
if (!fhirPath.value.trim()) { /* ... */ }
|
||||
const method = document.querySelector('input[name="method"]:checked')?.value;
|
||||
if (!method) { /* ... */ }
|
||||
// ...etc...
|
||||
|
||||
// Example: CSRF token retrieval inside handler
|
||||
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
|
||||
if (useLocalHapi && (method === 'POST' || method === 'PUT') && csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
} else if (useLocalHapi && (method === 'POST' || method === 'PUT')) {
|
||||
console.warn("CSRF token input not found for local request.");
|
||||
// Potentially alert the user or block the request
|
||||
}
|
||||
// ... rest of send logic ...
|
||||
});
|
||||
} else {
|
||||
console.error("Cannot attach listener: Send Request Button, FHIR Path input, or Form element not found.");
|
||||
}
|
||||
|
||||
}); // End DOMContentLoaded Listener
|
||||
</script>
|
||||
{% endblock %}
|
1759
templates/fhir_ui_operations.html
Normal file
1759
templates/fhir_ui_operations.html
Normal file
File diff suppressed because it is too large
Load Diff
1788
templates/fhir_ui_operations.html.bak
Normal file
1788
templates/fhir_ui_operations.html.bak
Normal file
File diff suppressed because it is too large
Load Diff
225
templates/fsh_converter.html
Normal file
225
templates/fsh_converter.html
Normal file
@ -0,0 +1,225 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_form_helpers.html" import render_field %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 py-5 my-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192">
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">FSH Converter</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
Convert FHIR JSON or XML resources to FHIR Shorthand (FSH) using GoFSH.
|
||||
</p>
|
||||
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">View Downloaded IGs</a>
|
||||
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate Sample</a>
|
||||
<a href="{{ url_for('fhir_ui_operations') }}" class="btn btn-outline-secondary btn-lg px-4">FHIR Operations</a>
|
||||
</div>
|
||||
-----------------------------------------------------------------remove the buttons----------------------------------------------------->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spinner Overlay -->
|
||||
<div id="spinner-overlay" class="d-none position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-50 d-flex align-items-center justify-content-center" style="z-index: 1050;">
|
||||
<div class="text-center">
|
||||
<div id="spinner-animation" style="width: 200px; height: 200px;"></div>
|
||||
<p class="text-white mt-3">Waiting, don't leave this page...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2><i class="bi bi-file-code me-2"></i>Convert FHIR to FSH</h2>
|
||||
<div id="fsh-output">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form id="fsh-converter-form" method="POST" enctype="multipart/form-data" class="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field(form.package) }}
|
||||
{{ render_field(form.input_mode) }}
|
||||
<div id="file-upload" style="display: none;">
|
||||
{{ render_field(form.fhir_file) }}
|
||||
</div>
|
||||
<div id="text-input" style="display: none;">
|
||||
{{ render_field(form.fhir_text) }}
|
||||
</div>
|
||||
{{ render_field(form.output_style) }}
|
||||
{{ render_field(form.log_level) }}
|
||||
{{ render_field(form.fhir_version) }}
|
||||
{{ render_field(form.fishing_trip) }}
|
||||
{{ render_field(form.dependencies, placeholder="One per line, e.g., hl7.fhir.us.core@6.1.0") }}
|
||||
{{ render_field(form.indent_rules) }}
|
||||
{{ render_field(form.meta_profile) }}
|
||||
{{ render_field(form.alias_file) }}
|
||||
{{ render_field(form.no_alias) }}
|
||||
<div class="d-grid gap-2 d-sm-flex">
|
||||
{{ form.submit(class="btn btn-success", id="submit-btn") }}
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/lottie.min.js') }}"></script>
|
||||
<script>
|
||||
// --- Global Variables and Helper Functions (Defined *outside* DOMContentLoaded) ---
|
||||
let currentLottieAnimation = null;
|
||||
|
||||
function loadLottieAnimation(theme) {
|
||||
// console.log(`loadLottieAnimation called with theme: ${theme}`); // Optional debug log
|
||||
const spinnerContainer = document.getElementById('spinner-animation');
|
||||
// console.log(`Found spinnerContainer: ${!!spinnerContainer}`); // Optional debug log
|
||||
if (!spinnerContainer) return;
|
||||
if (currentLottieAnimation) {
|
||||
// console.log("Destroying previous Lottie animation."); // Optional debug log
|
||||
currentLottieAnimation.destroy();
|
||||
currentLottieAnimation = null;
|
||||
}
|
||||
const animationPath = (theme === 'dark')
|
||||
? '{{ url_for('static', filename='animations/loading-dark.json') }}'
|
||||
: '{{ url_for('static', filename='animations/loading-light.json') }}';
|
||||
// console.log(`Determined animation path: ${animationPath}`); // Optional debug log
|
||||
try {
|
||||
currentLottieAnimation = lottie.loadAnimation({ container: spinnerContainer, renderer: 'svg', loop: true, autoplay: true, path: animationPath });
|
||||
// console.log("New Lottie animation loaded."); // Optional debug log
|
||||
} catch(lottieError) {
|
||||
console.error("Error loading Lottie animation:", lottieError);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Function to handle input mode changes (for SELECT element) ---
|
||||
function handleInputModeChange(selectElement) {
|
||||
if (!selectElement) { console.error("handleInputModeChange called with no selectElement"); return; }
|
||||
const selectedValue = selectElement.value; // Get value from the select element
|
||||
console.log(`handleInputModeChange called for value: ${selectedValue}`); // Keep this log for now
|
||||
|
||||
const form = selectElement.closest('form');
|
||||
if (!form) { console.error('Could not find parent form for input mode toggle.'); return; }
|
||||
|
||||
const fileUploadDiv = form.querySelector('#file-upload');
|
||||
const textInputDiv = form.querySelector('#text-input');
|
||||
// console.log(`Found fileUploadDiv: ${!!fileUploadDiv}, Found textInputDiv: ${!!textInputDiv}`); // Optional debug log
|
||||
|
||||
if (fileUploadDiv && textInputDiv) {
|
||||
// Show/hide based on the SELECTED VALUE
|
||||
if (selectedValue === 'file') {
|
||||
fileUploadDiv.style.display = 'block';
|
||||
textInputDiv.style.display = 'none';
|
||||
} else { // Assuming 'text' is the other value
|
||||
fileUploadDiv.style.display = 'none';
|
||||
textInputDiv.style.display = 'block';
|
||||
}
|
||||
// console.log(`Set display - file: ${fileUploadDiv.style.display}, text: ${textInputDiv.style.display}`); // Optional debug log
|
||||
} else {
|
||||
console.error('Could not find file-upload or text-input divs within the form.');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Function to initialize UI elements within a container (for SELECT element) ---
|
||||
function initializeFshConverterUI(containerElement) {
|
||||
console.log("Initializing FSH Converter UI within:", containerElement === document ? "document" : "outputContainer"); // Keep this log for now
|
||||
// --- Input Mode Toggle Initialization ---
|
||||
// Select the dropdown using its ID
|
||||
const inputModeSelect = containerElement.querySelector('#input_mode'); // Use ID selector for the select
|
||||
|
||||
if (inputModeSelect) {
|
||||
// Attach the listener directly to the select element
|
||||
inputModeSelect.addEventListener('change', (event) => {
|
||||
// Pass the select element itself to the handler
|
||||
handleInputModeChange(event.target);
|
||||
});
|
||||
|
||||
// Set initial state based on the select element's current value
|
||||
console.log(`Initial input mode detected: ${inputModeSelect.value}`); // Keep this log
|
||||
handleInputModeChange(inputModeSelect); // Call handler directly with the select element
|
||||
|
||||
} else {
|
||||
// This error should hopefully not appear now
|
||||
console.error('Input mode select element (#input_mode) not found within container:', containerElement);
|
||||
}
|
||||
|
||||
// Only load initial Lottie when initializing the whole document
|
||||
if (containerElement === document) {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
// Make sure loadLottieAnimation function exists before calling
|
||||
if (typeof loadLottieAnimation === 'function') {
|
||||
loadLottieAnimation(currentTheme);
|
||||
} else {
|
||||
console.error("loadLottieAnimation function not defined.");
|
||||
}
|
||||
}
|
||||
} // End initializeFshConverterUI
|
||||
|
||||
// --- Main Execution on DOMContentLoaded ---
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// --- Initial UI Setup ---
|
||||
// Use setTimeout for the initial call to ensure form elements are ready
|
||||
setTimeout(() => {
|
||||
initializeFshConverterUI(document);
|
||||
}, 50); // Using a slightly longer delay just in case 0 isn't enough
|
||||
|
||||
// --- Form submission with AJAX (Event Delegation) ---
|
||||
const outputContainer = document.getElementById('fsh-output');
|
||||
if (outputContainer) {
|
||||
outputContainer.addEventListener('submit', async function(event) {
|
||||
// Check if the event originated from our specific form
|
||||
if (event.target && event.target.id === 'fsh-converter-form') {
|
||||
event.preventDefault(); // Prevent default only if it's our form
|
||||
|
||||
const form = event.target;
|
||||
const submitBtn = form.querySelector('#submit-btn');
|
||||
const spinnerOverlay = document.getElementById('spinner-overlay'); // Spinner is outside the form
|
||||
|
||||
if (!submitBtn || !spinnerOverlay) {
|
||||
console.error("Submit button or spinner overlay not found.");
|
||||
return; // Stop if critical elements missing
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
spinnerOverlay.classList.remove('d-none'); // Show spinner
|
||||
|
||||
const formData = new FormData(form);
|
||||
try {
|
||||
const response = await fetch('{{ url_for('fsh_converter') }}', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: formData });
|
||||
if (!response.ok) { let errorText = response.statusText; try { const errorData = await response.json(); errorText = errorData.error || errorText; } catch (e) {} throw new Error(`Network response was not ok: ${response.status} ${errorText}`); }
|
||||
const html = await response.text();
|
||||
|
||||
const themeNow = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
// Check function exists before calling, just in case
|
||||
if (typeof loadLottieAnimation === 'function') {
|
||||
loadLottieAnimation(themeNow); // Ensure animation theme is updated before showing result
|
||||
}
|
||||
|
||||
outputContainer.innerHTML = html; // Replace content
|
||||
|
||||
initializeFshConverterUI(outputContainer); // Re-initialize controls for new content
|
||||
|
||||
// Hide spinner AFTER content replaced and potentially re-initialized
|
||||
spinnerOverlay.classList.add('d-none');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during FSH conversion AJAX:', error);
|
||||
if(spinnerOverlay) spinnerOverlay.classList.add('d-none');
|
||||
// No need to re-enable submitBtn as the form was replaced
|
||||
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'alert alert-danger mt-4';
|
||||
alertDiv.textContent = `An error occurred during conversion: ${error.message}. Please check input and try again.`;
|
||||
outputContainer.innerHTML = ''; // Clear previous content before showing only error
|
||||
outputContainer.appendChild(alertDiv);
|
||||
}
|
||||
} // End if event target is form
|
||||
}); // End submit listener
|
||||
} else {
|
||||
console.error("#fsh-output container not found for attaching delegated event listener.");
|
||||
}
|
||||
}); // End DOMContentLoaded Listener
|
||||
</script>
|
||||
{% endblock %}
|
@ -10,11 +10,13 @@
|
||||
<p class="lead mb-4">
|
||||
Import new FHIR Implementation Guides to the system for viewing.
|
||||
</p>
|
||||
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">View Downloaded IGs</a>
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
|
||||
</div>
|
||||
-----------------------------------------------------------------remove the buttons----------------------------------------------------->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -8,11 +8,15 @@
|
||||
<p class="lead mb-4">
|
||||
Simple tool for importing, viewing, and validating FHIR Implementation Guides.
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import FHIR IG</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">View Downloaded IGs</a>
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IGs</a>
|
||||
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center flex-wrap">
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3 mb-2">Import FHIR IG</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">Manage FHIR Packages</a>
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">Push IGs</a>
|
||||
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">Validate FHIR Sample</a>
|
||||
<a href="{{ url_for('fhir_ui') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">FHIR API Explorer</a>
|
||||
<a href="{{ url_for('fhir_ui_operations') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">FHIR UI Operations</a>
|
||||
<a href="{{ url_for('fsh_converter') }}" class="btn btn-outline-secondary btn-lg px-4 mb-2">FSH Converter</a>
|
||||
<a href="{{ url_for('about') }}" class="btn btn-outline-info btn-lg px-4 mb-2">About</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,10 +9,12 @@
|
||||
<p class="lead mb-4">
|
||||
Validate a FHIR resource or bundle against a selected Implementation Guide. (ALPHA - TEST Fhir pathing is complex and the logic is WIP, please report anomalies you find in Github issues alongside your sample json REMOVE PHI)
|
||||
</p>
|
||||
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
|
||||
</div>
|
||||
----------------------------------------------------------------remove the buttons----------------------------------------------------->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -101,12 +103,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function updatePackageFields(select) {
|
||||
const value = select.value;
|
||||
console.log('Selected package:', value);
|
||||
|
||||
if (!packageNameInput || !versionInput) {
|
||||
console.error('Input fields not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const [name, version] = value.split('#');
|
||||
if (name && version) {
|
||||
@ -162,6 +162,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Check HAPI server status
|
||||
function checkHapiStatus() {
|
||||
return fetch('/fhir/metadata', { method: 'GET' })
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HAPI server unavailable');
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('HAPI status check failed:', error.message);
|
||||
validationContent.innerHTML = '<div class="alert alert-warning">HAPI server unavailable; using local validation.</div>';
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize text to prevent XSS
|
||||
function sanitizeText(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Validate button handler
|
||||
if (validateButton) {
|
||||
validateButton.addEventListener('click', function() {
|
||||
@ -186,13 +209,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
JSON.parse(sampleData);
|
||||
} catch (e) {
|
||||
validationResult.style.display = 'block';
|
||||
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + e.message + '</div>';
|
||||
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
validationResult.style.display = 'block';
|
||||
validationContent.innerHTML = '<div class="text-center py-3"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Validating...</span></div></div>';
|
||||
|
||||
// Check HAPI status before validation
|
||||
checkHapiStatus().then(() => {
|
||||
console.log('Sending request to /api/validate-sample with:', {
|
||||
package_name: packageName,
|
||||
version: version,
|
||||
@ -228,64 +253,121 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
validationContent.innerHTML = '';
|
||||
let hasContent = false;
|
||||
|
||||
// Display profile information
|
||||
if (data.results) {
|
||||
const profileDiv = document.createElement('div');
|
||||
profileDiv.className = 'mb-3';
|
||||
const profiles = new Set();
|
||||
Object.values(data.results).forEach(result => {
|
||||
if (result.profile) profiles.add(result.profile);
|
||||
});
|
||||
if (profiles.size > 0) {
|
||||
profileDiv.innerHTML = `
|
||||
<h5>Validated Against Profile${profiles.size > 1 ? 's' : ''}</h5>
|
||||
<ul>
|
||||
${Array.from(profiles).map(p => `<li><a href="${sanitizeText(p)}" target="_blank">${sanitizeText(p.split('/').pop())}</a></li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
validationContent.appendChild(profileDiv);
|
||||
hasContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Display errors
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
hasContent = true;
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'alert alert-danger';
|
||||
errorDiv.innerHTML = '<h5>Errors</h5><ul>' +
|
||||
data.errors.map(e => `<li>${typeof e === 'string' ? e : e.message}</li>`).join('') +
|
||||
'</ul>';
|
||||
errorDiv.innerHTML = `
|
||||
<h5>Errors</h5>
|
||||
<ul>
|
||||
${data.errors.map(e => `<li>${sanitizeText(typeof e === 'string' ? e : e.message)}</li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
validationContent.appendChild(errorDiv);
|
||||
}
|
||||
|
||||
// Display warnings
|
||||
if (data.warnings && data.warnings.length > 0) {
|
||||
hasContent = true;
|
||||
const warningDiv = document.createElement('div');
|
||||
warningDiv.className = 'alert alert-warning';
|
||||
warningDiv.innerHTML = '<h5>Warnings</h5><ul>' +
|
||||
data.warnings.map(w => `<li>${typeof w === 'string' ? w : w.message}</li>`).join('') +
|
||||
'</ul>';
|
||||
warningDiv.innerHTML = `
|
||||
<h5>Warnings</h5>
|
||||
<ul>
|
||||
${data.warnings.map(w => {
|
||||
const isMustSupport = w.includes('Must Support');
|
||||
return `<li>${sanitizeText(typeof w === 'string' ? w : w.message)}${isMustSupport ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}</li>`;
|
||||
}).join('')}
|
||||
</ul>
|
||||
`;
|
||||
validationContent.appendChild(warningDiv);
|
||||
}
|
||||
|
||||
// Detailed results with collapsible sections
|
||||
if (data.results) {
|
||||
hasContent = true;
|
||||
const resultsDiv = document.createElement('div');
|
||||
resultsDiv.innerHTML = '<h5>Detailed Results</h5>';
|
||||
let index = 0;
|
||||
for (const [resourceId, result] of Object.entries(data.results)) {
|
||||
const collapseId = `result-collapse-${index++}`;
|
||||
resultsDiv.innerHTML += `
|
||||
<h6>${resourceId}</h6>
|
||||
<p><strong>Valid:</strong> ${result.valid ? 'Yes' : 'No'}</p>
|
||||
<div class="card mb-2">
|
||||
<div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
||||
<h6 class="mb-0">${sanitizeText(resourceId)}</h6>
|
||||
<p class="mb-0"><strong>Valid:</strong> ${result.valid ? 'Yes' : 'No'}</p>
|
||||
</div>
|
||||
<div id="${collapseId}" class="collapse">
|
||||
<div class="card-body">
|
||||
${result.details && result.details.length > 0 ? `
|
||||
<ul>
|
||||
${result.details.map(d => `
|
||||
<li class="${d.severity === 'error' ? 'text-danger' : d.severity === 'warning' ? 'text-warning' : 'text-info'}">
|
||||
${sanitizeText(d.issue)}
|
||||
${d.description && d.description !== d.issue ? `<br><small class="text-muted">${sanitizeText(d.description)}</small>` : ''}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
` : '<p>No additional details.</p>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
resultsDiv.innerHTML += '<ul class="text-danger">' +
|
||||
result.errors.map(e => `<li>${typeof e === 'string' ? e : e.message}</li>`).join('') +
|
||||
'</ul>';
|
||||
}
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
resultsDiv.innerHTML += '<ul class="text-warning">' +
|
||||
result.warnings.map(w => `<li>${typeof w === 'string' ? w : w.message}</li>`).join('') +
|
||||
'</ul>';
|
||||
}
|
||||
}
|
||||
validationContent.appendChild(resultsDiv);
|
||||
}
|
||||
|
||||
// Summary
|
||||
const summaryDiv = document.createElement('div');
|
||||
summaryDiv.className = 'alert alert-info';
|
||||
summaryDiv.innerHTML = `<h5>Summary</h5><p>Valid: ${data.valid ? 'Yes' : 'No'}</p>`;
|
||||
if (data.summary && (data.summary.error_count || data.summary.warning_count)) {
|
||||
summaryDiv.innerHTML += `<p>Errors: ${data.summary.error_count || 0}, Warnings: ${data.summary.warning_count || 0}</p>`;
|
||||
}
|
||||
summaryDiv.innerHTML = `
|
||||
<h5>Summary</h5>
|
||||
<p>Valid: ${data.valid ? 'Yes' : 'No'}</p>
|
||||
<p>Errors: ${data.summary.error_count || 0}, Warnings: ${data.summary.warning_count || 0}</p>
|
||||
`;
|
||||
validationContent.appendChild(summaryDiv);
|
||||
hasContent = true;
|
||||
|
||||
if (!hasContent && data.valid) {
|
||||
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
|
||||
}
|
||||
|
||||
// Initialize Bootstrap collapse and tooltips
|
||||
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const target = document.querySelector(el.getAttribute('data-bs-target'));
|
||||
target.classList.toggle('show');
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
||||
new bootstrap.Tooltip(el);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Validation error:', error);
|
||||
validationContent.innerHTML = '<div class="alert alert-danger">Error during validation: ' + error.message + '</div>';
|
||||
validationContent.innerHTML = `<div class="alert alert-danger">Error during validation: ${sanitizeText(error.message)}</div>`;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
155
templates/woooo
155
templates/woooo
@ -1,155 +0,0 @@
|
||||
// Function to build the hierarchical data structure for the tree
|
||||
function buildTreeData(elements) {
|
||||
const treeRoot = { children: {}, element: null, name: 'Root' };
|
||||
const nodeMap = { 'Root': treeRoot };
|
||||
|
||||
console.log("Building tree with elements:", elements.length);
|
||||
elements.forEach((el, index) => {
|
||||
const path = el.path;
|
||||
const id = el.id || null;
|
||||
const sliceName = el.sliceName || null;
|
||||
|
||||
console.log(`Element ${index}: path=${path}, id=${id}, sliceName=${sliceName}`);
|
||||
|
||||
if (!path) {
|
||||
console.warn(`Skipping element ${index} with no path`);
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = path.split('.');
|
||||
let currentPath = '';
|
||||
let parentNode = treeRoot;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
currentPath = i === 0 ? part : `${currentPath}.${part}`;
|
||||
|
||||
// For extensions, append sliceName to path if present
|
||||
let nodeKey = part;
|
||||
if (part === 'extension' && i === parts.length - 1 && sliceName) {
|
||||
nodeKey = `${part}:${sliceName}`;
|
||||
currentPath = id || currentPath; // Use id for precise matching in extensions
|
||||
}
|
||||
|
||||
if (!nodeMap[currentPath]) {
|
||||
const newNode = {
|
||||
children: {},
|
||||
element: null,
|
||||
name: nodeKey,
|
||||
path: currentPath
|
||||
};
|
||||
let parentPath = i === 0 ? 'Root' : parts.slice(0, i).join('.');
|
||||
parentNode = nodeMap[parentPath] || treeRoot;
|
||||
parentNode.children[nodeKey] = newNode;
|
||||
nodeMap[currentPath] = newNode;
|
||||
console.log(`Created node: path=${currentPath}, name=${nodeKey}`);
|
||||
}
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
const targetNode = nodeMap[currentPath];
|
||||
targetNode.element = el;
|
||||
console.log(`Assigned element to node: path=${currentPath}, id=${id}, sliceName=${sliceName}`);
|
||||
}
|
||||
|
||||
parentNode = nodeMap[currentPath];
|
||||
}
|
||||
});
|
||||
|
||||
const treeData = Object.values(treeRoot.children);
|
||||
console.log("Tree data constructed:", treeData);
|
||||
return treeData;
|
||||
}
|
||||
|
||||
// Function to render a single node (and its children recursively) as an <li>
|
||||
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
|
||||
if (!node || !node.element) {
|
||||
console.warn("Skipping render for invalid node:", node);
|
||||
return '';
|
||||
}
|
||||
const el = node.element;
|
||||
const path = el.path || 'N/A';
|
||||
const id = el.id || null;
|
||||
const sliceName = el.sliceName || null;
|
||||
const min = el.min !== undefined ? el.min : '';
|
||||
const max = el.max || '';
|
||||
const short = el.short || '';
|
||||
const definition = el.definition || '';
|
||||
|
||||
console.log(`Rendering node: path=${path}, id=${id}, sliceName=${sliceName}`);
|
||||
console.log(` MustSupportPathsSet contains path: ${mustSupportPathsSet.has(path)}`);
|
||||
if (id) {
|
||||
console.log(` MustSupportPathsSet contains id: ${mustSupportPathsSet.has(id)}`);
|
||||
}
|
||||
|
||||
// Check MS for path, id, or normalized extension path
|
||||
let isMustSupport = mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id));
|
||||
if (!isMustSupport && path.startsWith('Extension.extension')) {
|
||||
const basePath = path.split(':')[0];
|
||||
const baseId = id ? id.split(':')[0] : null;
|
||||
isMustSupport = mustSupportPathsSet.has(basePath) || (baseId && mustSupportPathsSet.has(baseId));
|
||||
console.log(` Extension check: basePath=${basePath}, baseId=${baseId}, isMustSupport=${isMustSupport}`);
|
||||
}
|
||||
|
||||
console.log(` Final isMustSupport for ${path}: ${isMustSupport}`);
|
||||
|
||||
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
||||
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning ms-1" title="Must Support"></i>' : '';
|
||||
const hasChildren = Object.keys(node.children).length > 0;
|
||||
const collapseId = `collapse-${path.replace(/[\.\:\/\[\]\(\)]/g, '-')}`;
|
||||
const padding = level * 20;
|
||||
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
||||
|
||||
let typeString = 'N/A';
|
||||
if (el.type && el.type.length > 0) {
|
||||
typeString = el.type.map(t => {
|
||||
let s = t.code || '';
|
||||
let profiles = t.targetProfile || t.profile || [];
|
||||
if (profiles.length > 0) {
|
||||
const targetTypes = profiles.map(p => (p || '').split('/').pop()).filter(Boolean).join(', ');
|
||||
if (targetTypes) {
|
||||
s += `(<span class="text-muted fst-italic" title="${profiles.join(', ')}">${targetTypes}</span>)`;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}).join(' | ');
|
||||
}
|
||||
|
||||
let childrenHtml = '';
|
||||
if (hasChildren) {
|
||||
childrenHtml += `<ul class="collapse list-group list-group-flush structure-subtree" id="${collapseId}">`;
|
||||
Object.values(node.children).sort((a, b) => (a.element?.path ?? a.name).localeCompare(b.element?.path ?? b.name)).forEach(childNode => {
|
||||
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, level + 1);
|
||||
});
|
||||
childrenHtml += `</ul>`;
|
||||
}
|
||||
|
||||
let itemHtml = `<li class="${liClass}">`;
|
||||
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" data-bs-target="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}">`;
|
||||
itemHtml += `<span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
||||
if (hasChildren) {
|
||||
itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
||||
}
|
||||
itemHtml += `</span>`;
|
||||
itemHtml += `<code class="fw-bold ms-1" title="${path}">${node.name}</code>`;
|
||||
itemHtml += `</div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center text-muted small"><code>${min}..${max}</code></div>`;
|
||||
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
|
||||
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
|
||||
let descriptionTooltipAttrs = '';
|
||||
if (definition) {
|
||||
const escapedDefinition = definition
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n/g, ' ');
|
||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||
}
|
||||
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short || (definition ? '(definition only)' : '')}</div>`;
|
||||
itemHtml += `</div>`;
|
||||
itemHtml += childrenHtml;
|
||||
itemHtml += `</li>`;
|
||||
return itemHtml;
|
||||
}
|
@ -5,41 +5,31 @@ import json
|
||||
import tarfile
|
||||
import shutil
|
||||
import io
|
||||
# Use import requests early to ensure it's available for exceptions if needed
|
||||
import requests
|
||||
# Added patch, MagicMock etc. Also patch.object
|
||||
from unittest.mock import patch, MagicMock, mock_open, call
|
||||
# Added session import for flash message checking, timezone for datetime
|
||||
from flask import Flask, session, render_template
|
||||
from flask import Flask, session
|
||||
from flask.testing import FlaskClient
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Add the parent directory (/app) to sys.path
|
||||
# Ensure this points correctly to the directory containing 'app.py' and 'services.py'
|
||||
APP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
if APP_DIR not in sys.path:
|
||||
sys.path.insert(0, APP_DIR)
|
||||
|
||||
# Import app and models AFTER potentially modifying sys.path
|
||||
# Assuming app.py and services.py are in APP_DIR
|
||||
from app import app, db, ProcessedIg
|
||||
import services # Import the module itself for patch.object
|
||||
import services
|
||||
|
||||
# Helper function to parse NDJSON stream
|
||||
def parse_ndjson(byte_stream):
|
||||
"""Parses a byte stream of NDJSON into a list of Python objects."""
|
||||
decoded_stream = byte_stream.decode('utf-8').strip()
|
||||
if not decoded_stream:
|
||||
return []
|
||||
lines = decoded_stream.split('\n')
|
||||
# Filter out empty lines before parsing
|
||||
return [json.loads(line) for line in lines if line.strip()]
|
||||
|
||||
class TestFHIRFlareIGToolkit(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Configure the Flask app for testing ONCE for the entire test class."""
|
||||
app.config['TESTING'] = True
|
||||
app.config['WTF_CSRF_ENABLED'] = False
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||
@ -55,16 +45,13 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
||||
db.create_all()
|
||||
cls.client = app.test_client()
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Clean up DB and context after all tests."""
|
||||
cls.app_context.pop()
|
||||
if os.path.exists(cls.test_packages_dir):
|
||||
shutil.rmtree(cls.test_packages_dir)
|
||||
|
||||
def setUp(self):
|
||||
"""Set up before each test method."""
|
||||
if os.path.exists(self.test_packages_dir):
|
||||
shutil.rmtree(self.test_packages_dir)
|
||||
os.makedirs(self.test_packages_dir, exist_ok=True)
|
||||
@ -74,32 +61,62 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
||||
db.session.commit()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test method."""
|
||||
pass
|
||||
|
||||
# --- Helper Method ---
|
||||
# Helper Method
|
||||
def create_mock_tgz(self, filename, files_content):
|
||||
"""Creates a mock .tgz file with specified contents."""
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
with tarfile.open(tgz_path, "w:gz") as tar:
|
||||
for name, content in files_content.items():
|
||||
if isinstance(content, (dict, list)): data_bytes = json.dumps(content).encode('utf-8')
|
||||
elif isinstance(content, str): data_bytes = content.encode('utf-8')
|
||||
else: raise TypeError(f"Unsupported type for mock file '{name}': {type(content)}")
|
||||
if isinstance(content, (dict, list)):
|
||||
data_bytes = json.dumps(content).encode('utf-8')
|
||||
elif isinstance(content, str):
|
||||
data_bytes = content.encode('utf-8')
|
||||
else:
|
||||
raise TypeError(f"Unsupported type for mock file '{name}': {type(content)}")
|
||||
file_io = io.BytesIO(data_bytes)
|
||||
tarinfo = tarfile.TarInfo(name=name); tarinfo.size = len(data_bytes)
|
||||
tarinfo = tarfile.TarInfo(name=name)
|
||||
tarinfo.size = len(data_bytes)
|
||||
tarinfo.mtime = int(datetime.now(timezone.utc).timestamp())
|
||||
tar.addfile(tarinfo, file_io)
|
||||
return tgz_path
|
||||
|
||||
# --- Phase 1 Tests ---
|
||||
|
||||
def test_01_navigate_fhir_path(self):
|
||||
resource = {
|
||||
"resourceType": "Patient",
|
||||
"name": [{"given": ["John"]}],
|
||||
"identifier": [{"system": "http://hl7.org/fhir/sid/us-ssn", "sliceName": "us-ssn"}],
|
||||
"extension": [{"url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", "valueAddress": {"city": "Boston"}}]
|
||||
}
|
||||
self.assertEqual(services.navigate_fhir_path(resource, "Patient.name[0].given"), ["John"])
|
||||
self.assertEqual(services.navigate_fhir_path(resource, "Patient.identifier:us-ssn.system"), "http://hl7.org/fhir/sid/us-ssn")
|
||||
self.assertEqual(services.navigate_fhir_path(resource, "Patient.extension", extension_url="http://hl7.org/fhir/StructureDefinition/patient-birthPlace")["valueAddress"]["city"], "Boston")
|
||||
with patch('fhirpath.evaluate', side_effect=Exception("fhirpath error")):
|
||||
self.assertEqual(services.navigate_fhir_path(resource, "Patient.name[0].given"), ["John"])
|
||||
|
||||
def test_02_render_node_as_li(self):
|
||||
node = {
|
||||
"element": {"path": "Patient.identifier", "id": "Patient.identifier", "sliceName": "us-ssn", "min": 0, "max": "*", "type": [{"code": "Identifier"}]},
|
||||
"name": "identifier",
|
||||
"children": {}
|
||||
}
|
||||
must_support_paths = {"Patient.identifier:us-ssn"}
|
||||
with app.app_context:
|
||||
html = render_template('cp_view_processed_ig.html', processed_ig=MagicMock(must_support_elements={"USCorePatientProfile": ["Patient.identifier:us-ssn"]}), profile_list=[{"name": "USCorePatientProfile"}], base_list=[])
|
||||
self.assertIn("identifier:us-ssn", html)
|
||||
self.assertIn("list-group-item-warning", html)
|
||||
self.assertIn("Must Support (Slice: us-ssn)", html)
|
||||
|
||||
# --- Basic Page Rendering Tests ---
|
||||
|
||||
def test_01_homepage(self):
|
||||
def test_03_homepage(self):
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'FHIRFLARE IG Toolkit', response.data)
|
||||
|
||||
def test_02_import_ig_page(self):
|
||||
def test_04_import_ig_page(self):
|
||||
response = self.client.get('/import-ig')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Import IG', response.data)
|
||||
@ -108,23 +125,23 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
||||
self.assertIn(b'name="dependency_mode"', response.data)
|
||||
|
||||
@patch('app.list_downloaded_packages', return_value=([], [], {}))
|
||||
def test_03_view_igs_no_packages(self, mock_list_pkgs):
|
||||
def test_05_view_igs_no_packages(self, mock_list_pkgs):
|
||||
response = self.client.get('/view-igs')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(b'<th>Package Name</th>', response.data)
|
||||
self.assertIn(b'No packages downloaded yet.', response.data)
|
||||
mock_list_pkgs.assert_called_once()
|
||||
|
||||
def test_04_view_igs_with_packages(self):
|
||||
self.create_mock_tgz('hl7.fhir.us.core-3.1.1.tgz', {'package/package.json': {'name': 'hl7.fhir.us.core', 'version': '3.1.1'}})
|
||||
def test_06_view_igs_with_packages(self):
|
||||
self.create_mock_tgz('hl7.fhir.us.core-6.1.0.tgz', {'package/package.json': {'name': 'hl7.fhir.us.core', 'version': '6.1.0'}})
|
||||
response = self.client.get('/view-igs')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'hl7.fhir.us.core', response.data)
|
||||
self.assertIn(b'3.1.1', response.data)
|
||||
self.assertIn(b'6.1.0', response.data)
|
||||
self.assertIn(b'<th>Package Name</th>', response.data)
|
||||
|
||||
@patch('app.render_template')
|
||||
def test_05_push_igs_page(self, mock_render_template):
|
||||
def test_07_push_igs_page(self, mock_render_template):
|
||||
mock_render_template.return_value = "Mock Render"
|
||||
response = self.client.get('/push-igs')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@ -136,285 +153,636 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
||||
|
||||
@patch('app.services.import_package_and_dependencies')
|
||||
def test_10_import_ig_form_success(self, mock_import):
|
||||
mock_import.return_value = { 'requested': ('hl7.fhir.us.core', '3.1.1'), 'processed': {('hl7.fhir.us.core', '3.1.1')}, 'downloaded': {('hl7.fhir.us.core', '3.1.1'): 'path/pkg.tgz'}, 'all_dependencies': {}, 'dependencies': [], 'errors': [] }
|
||||
response = self.client.post('/import-ig', data={'package_name': 'hl7.fhir.us.core', 'package_version': '3.1.1', 'dependency_mode': 'recursive'}, follow_redirects=True)
|
||||
self.assertEqual(response.status_code, 200); self.assertIn(b'Successfully downloaded hl7.fhir.us.core#3.1.1 and dependencies! Mode: recursive', response.data)
|
||||
mock_import.assert_called_once_with('hl7.fhir.us.core', '3.1.1', dependency_mode='recursive')
|
||||
mock_import.return_value = {'requested': ('hl7.fhir.us.core', '6.1.0'), 'processed': {('hl7.fhir.us.core', '6.1.0')}, 'downloaded': {('hl7.fhir.us.core', '6.1.0'): 'path/pkg.tgz'}, 'all_dependencies': {}, 'dependencies': [], 'errors': []}
|
||||
response = self.client.post('/import-ig', data={'package_name': 'hl7.fhir.us.core', 'package_version': '6.1.0', 'dependency_mode': 'recursive'}, follow_redirects=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Successfully downloaded hl7.fhir.us.core#6.1.0 and dependencies! Mode: recursive', response.data)
|
||||
mock_import.assert_called_once_with('hl7.fhir.us.core', '6.1.0', dependency_mode='recursive')
|
||||
|
||||
@patch('app.services.import_package_and_dependencies')
|
||||
def test_11_import_ig_form_failure_404(self, mock_import):
|
||||
mock_import.return_value = {'requested': ('invalid.package', '1.0.0'), 'processed': set(), 'downloaded': {}, 'all_dependencies': {}, 'dependencies': [], 'errors': ['HTTP error fetching package: 404 Client Error: Not Found for url: ...']}
|
||||
response = self.client.post('/import-ig', data={'package_name': 'invalid.package', 'package_version': '1.0.0', 'dependency_mode': 'recursive'}, follow_redirects=False)
|
||||
self.assertEqual(response.status_code, 200); self.assertIn(b'Package not found on registry (404)', response.data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Package not found on registry (404)', response.data)
|
||||
|
||||
@patch('app.services.import_package_and_dependencies')
|
||||
def test_12_import_ig_form_failure_conn_error(self, mock_import):
|
||||
mock_import.return_value = {'requested': ('conn.error.pkg', '1.0.0'), 'processed': set(), 'downloaded': {}, 'all_dependencies': {}, 'dependencies': [], 'errors': ['Connection error: Cannot connect to registry...']}
|
||||
response = self.client.post('/import-ig', data={'package_name': 'conn.error.pkg', 'package_version': '1.0.0', 'dependency_mode': 'recursive'}, follow_redirects=False)
|
||||
self.assertEqual(response.status_code, 200); self.assertIn(b'Could not connect to the FHIR package registry', response.data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Could not connect to the FHIR package registry', response.data)
|
||||
|
||||
def test_13_import_ig_form_invalid_input(self):
|
||||
response = self.client.post('/import-ig', data={'package_name': 'invalid@package', 'package_version': '1.0.0', 'dependency_mode': 'recursive'}, follow_redirects=True)
|
||||
self.assertEqual(response.status_code, 200); self.assertIn(b'Error in Package Name: Invalid package name format.', response.data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Error in Package Name: Invalid package name format.', response.data)
|
||||
|
||||
@patch('app.services.process_package_file')
|
||||
@patch('app.services.parse_package_filename')
|
||||
def test_20_process_ig_success(self, mock_parse, mock_process):
|
||||
pkg_name='hl7.fhir.us.core'; pkg_version='3.1.1'; filename=f'{pkg_name}-{pkg_version}.tgz'
|
||||
pkg_name = 'hl7.fhir.us.core'
|
||||
pkg_version = '6.1.0'
|
||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||
mock_parse.return_value = (pkg_name, pkg_version)
|
||||
mock_process.return_value = { 'resource_types_info': [{'name': 'Patient', 'type': 'Patient', 'is_profile': False, 'must_support': True, 'optional_usage': False}], 'must_support_elements': {'Patient': ['Patient.name']}, 'examples': {'Patient': ['package/Patient-example.json']}, 'complies_with_profiles': [], 'imposed_profiles': ['http://hl7.org/fhir/StructureDefinition/Patient'], 'errors': [] }
|
||||
mock_process.return_value = {
|
||||
'resource_types_info': [{'name': 'Patient', 'type': 'Patient', 'is_profile': False, 'must_support': True, 'optional_usage': False}],
|
||||
'must_support_elements': {'Patient': ['Patient.name', 'Patient.identifier:us-ssn']},
|
||||
'examples': {'Patient': ['package/Patient-example.json']},
|
||||
'complies_with_profiles': [],
|
||||
'imposed_profiles': ['http://hl7.org/fhir/StructureDefinition/Patient'],
|
||||
'errors': []
|
||||
}
|
||||
self.create_mock_tgz(filename, {'package/package.json': {'name': pkg_name, 'version': pkg_version}})
|
||||
response = self.client.post('/process-igs', data={'filename': filename}, follow_redirects=False)
|
||||
self.assertEqual(response.status_code, 302); self.assertTrue(response.location.endswith('/view-igs'))
|
||||
with self.client.session_transaction() as sess: self.assertIn(('success', f'Successfully processed {pkg_name}#{pkg_version}!'), sess.get('_flashes', []))
|
||||
mock_parse.assert_called_once_with(filename); mock_process.assert_called_once_with(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename))
|
||||
processed_ig = db.session.query(ProcessedIg).filter_by(package_name=pkg_name, version=pkg_version).first(); self.assertIsNotNone(processed_ig); self.assertEqual(processed_ig.package_name, pkg_name)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.location.endswith('/view-igs'))
|
||||
with self.client.session_transaction() as sess:
|
||||
self.assertIn(('success', f'Successfully processed {pkg_name}#{pkg_version}!'), sess.get('_flashes', []))
|
||||
mock_parse.assert_called_once_with(filename)
|
||||
mock_process.assert_called_once_with(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename))
|
||||
processed_ig = db.session.query(ProcessedIg).filter_by(package_name=pkg_name, version=pkg_version).first()
|
||||
self.assertIsNotNone(processed_ig)
|
||||
self.assertEqual(processed_ig.package_name, pkg_name)
|
||||
self.assertIn('Patient.name', processed_ig.must_support_elements.get('Patient', []))
|
||||
|
||||
def test_21_process_ig_file_not_found(self):
|
||||
response = self.client.post('/process-igs', data={'filename': 'nonexistent.tgz'}, follow_redirects=True)
|
||||
self.assertEqual(response.status_code, 200); self.assertIn(b'Package file not found: nonexistent.tgz', response.data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Package file not found: nonexistent.tgz', response.data)
|
||||
|
||||
def test_22_delete_ig_success(self):
|
||||
filename='hl7.fhir.us.core-3.1.1.tgz'; metadata_filename='hl7.fhir.us.core-3.1.1.metadata.json'
|
||||
self.create_mock_tgz(filename, {'package/package.json': {'name': 'hl7.fhir.us.core', 'version': '3.1.1'}})
|
||||
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename); open(metadata_path, 'w').write(json.dumps({'name': 'hl7.fhir.us.core'}))
|
||||
self.assertTrue(os.path.exists(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename))); self.assertTrue(os.path.exists(metadata_path))
|
||||
filename = 'hl7.fhir.us.core-6.1.0.tgz'
|
||||
metadata_filename = 'hl7.fhir.us.core-6.1.0.metadata.json'
|
||||
self.create_mock_tgz(filename, {'package/package.json': {'name': 'hl7.fhir.us.core', 'version': '6.1.0'}})
|
||||
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename)
|
||||
open(metadata_path, 'w').write(json.dumps({'name': 'hl7.fhir.us.core'}))
|
||||
self.assertTrue(os.path.exists(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)))
|
||||
self.assertTrue(os.path.exists(metadata_path))
|
||||
response = self.client.post('/delete-ig', data={'filename': filename}, follow_redirects=True)
|
||||
self.assertEqual(response.status_code, 200); self.assertIn(f'Deleted: {filename}, {metadata_filename}'.encode('utf-8'), response.data)
|
||||
self.assertFalse(os.path.exists(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename))); self.assertFalse(os.path.exists(metadata_path))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(f'Deleted: {filename}, {metadata_filename}'.encode('utf-8'), response.data)
|
||||
self.assertFalse(os.path.exists(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)))
|
||||
self.assertFalse(os.path.exists(metadata_path))
|
||||
|
||||
def test_23_unload_ig_success(self):
|
||||
processed_ig = ProcessedIg(package_name='test.pkg', version='1.0', processed_date=datetime.now(timezone.utc), resource_types_info=[], must_support_elements={}, examples={})
|
||||
db.session.add(processed_ig); db.session.commit(); ig_id = processed_ig.id; self.assertIsNotNone(db.session.get(ProcessedIg, ig_id))
|
||||
db.session.add(processed_ig)
|
||||
db.session.commit()
|
||||
ig_id = processed_ig.id
|
||||
self.assertIsNotNone(db.session.get(ProcessedIg, ig_id))
|
||||
response = self.client.post('/unload-ig', data={'ig_id': str(ig_id)}, follow_redirects=True)
|
||||
self.assertEqual(response.status_code, 200); self.assertIn(b'Unloaded processed data for test.pkg#1.0', response.data); self.assertIsNone(db.session.get(ProcessedIg, ig_id))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Unloaded processed data for test.pkg#1.0', response.data)
|
||||
self.assertIsNone(db.session.get(ProcessedIg, ig_id))
|
||||
|
||||
# --- API Tests ---
|
||||
# --- Phase 2 Tests ---
|
||||
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('tarfile.open')
|
||||
@patch('requests.put')
|
||||
def test_30_load_ig_to_hapi_success(self, mock_requests_put, mock_tarfile_open, mock_os_exists):
|
||||
pkg_name = 'hl7.fhir.us.core'
|
||||
pkg_version = '6.1.0'
|
||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||
self.create_mock_tgz(filename, {
|
||||
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||
'package/Patient-profile.json': {'resourceType': 'StructureDefinition', 'id': 'us-core-patient'}
|
||||
})
|
||||
mock_tar = MagicMock()
|
||||
profile_member = MagicMock(spec=tarfile.TarInfo)
|
||||
profile_member.name = 'package/Patient-profile.json'
|
||||
profile_member.isfile.return_value = True
|
||||
mock_tar.getmembers.return_value = [profile_member]
|
||||
mock_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'StructureDefinition', 'id': 'us-core-patient'}).encode('utf-8'))
|
||||
mock_tarfile_open.return_value.__enter__.return_value = mock_tar
|
||||
mock_requests_put.return_value = MagicMock(status_code=200)
|
||||
response = self.client.post(
|
||||
'/api/load-ig-to-hapi',
|
||||
data=json.dumps({'package_name': pkg_name, 'version': pkg_version}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['status'], 'success')
|
||||
mock_requests_put.assert_called_once_with(
|
||||
'http://localhost:8080/fhir/StructureDefinition/us-core-patient',
|
||||
json={'resourceType': 'StructureDefinition', 'id': 'us-core-patient'},
|
||||
headers={'Content-Type': 'application/fhir+json'}
|
||||
)
|
||||
|
||||
def test_31_load_ig_to_hapi_not_found(self):
|
||||
response = self.client.post(
|
||||
'/api/load-ig-to-hapi',
|
||||
data=json.dumps({'package_name': 'nonexistent', 'version': '1.0'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['error'], 'Package not found')
|
||||
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('requests.post')
|
||||
def test_32_api_validate_sample_hapi_success(self, mock_requests_post, mock_os_exists):
|
||||
pkg_name = 'hl7.fhir.us.core'
|
||||
pkg_version = '6.1.0'
|
||||
sample_resource = {
|
||||
'resourceType': 'Patient',
|
||||
'id': 'valid1',
|
||||
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']},
|
||||
'name': [{'given': ['John'], 'family': 'Doe'}]
|
||||
}
|
||||
mock_requests_post.return_value = MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {
|
||||
'resourceType': 'OperationOutcome',
|
||||
'issue': [{'severity': 'warning', 'diagnostics': 'Must Support element Patient.identifier missing'}]
|
||||
}
|
||||
)
|
||||
response = self.client.post(
|
||||
'/api/validate-sample',
|
||||
data=json.dumps({
|
||||
'package_name': pkg_name,
|
||||
'version': pkg_version,
|
||||
'sample_data': json.dumps(sample_resource),
|
||||
'mode': 'single',
|
||||
'include_dependencies': True
|
||||
}),
|
||||
content_type='application/json',
|
||||
headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertTrue(data['valid'])
|
||||
self.assertEqual(data['warnings'], ['Must Support element Patient.identifier missing'])
|
||||
mock_requests_post.assert_called_once_with(
|
||||
'http://localhost:8080/fhir/Patient/$validate?profile=http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient',
|
||||
json=sample_resource,
|
||||
headers={'Content-Type': 'application/fhir+json', 'Accept': 'application/fhir+json'},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('requests.post', side_effect=requests.ConnectionError("HAPI down"))
|
||||
@patch('services.navigate_fhir_path')
|
||||
def test_33_api_validate_sample_hapi_fallback(self, mock_navigate_fhir_path, mock_requests_post, mock_os_exists):
|
||||
pkg_name = 'hl7.fhir.us.core'
|
||||
pkg_version = '6.1.0'
|
||||
sample_resource = {
|
||||
'resourceType': 'Patient',
|
||||
'id': 'valid1',
|
||||
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']}
|
||||
}
|
||||
mock_navigate_fhir_path.return_value = None
|
||||
self.create_mock_tgz(f'{pkg_name}-{pkg_version}.tgz', {
|
||||
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||
'package/StructureDefinition-us-core-patient.json': {
|
||||
'resourceType': 'StructureDefinition',
|
||||
'snapshot': {'element': [{'path': 'Patient.name', 'min': 1}, {'path': 'Patient.identifier', 'mustSupport': True}]}
|
||||
}
|
||||
})
|
||||
response = self.client.post(
|
||||
'/api/validate-sample',
|
||||
data=json.dumps({
|
||||
'package_name': pkg_name,
|
||||
'version': pkg_version,
|
||||
'sample_data': json.dumps(sample_resource),
|
||||
'mode': 'single',
|
||||
'include_dependencies': True
|
||||
}),
|
||||
content_type='application/json',
|
||||
headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertFalse(data['valid'])
|
||||
self.assertIn('Required element Patient.name missing', data['errors'])
|
||||
self.assertIn('HAPI validation failed', [d['issue'] for d in data['details']])
|
||||
|
||||
# --- Phase 3 Tests ---
|
||||
|
||||
@patch('requests.get')
|
||||
def test_34_hapi_status_check(self, mock_requests_get):
|
||||
mock_requests_get.return_value = MagicMock(status_code=200, json=lambda: {'resourceType': 'CapabilityStatement'})
|
||||
response = self.client.get('/fhir/metadata')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['resourceType'], 'CapabilityStatement')
|
||||
mock_requests_get.side_effect = requests.ConnectionError("HAPI down")
|
||||
response = self.client.get('/fhir/metadata')
|
||||
self.assertEqual(response.status_code, 503)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('Unable to connect to HAPI FHIR server', data['error'])
|
||||
|
||||
def test_35_validate_sample_ui_rendering(self):
|
||||
pkg_name = 'hl7.fhir.us.core'
|
||||
pkg_version = '6.1.0'
|
||||
sample_resource = {
|
||||
'resourceType': 'Patient',
|
||||
'id': 'test',
|
||||
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']}
|
||||
}
|
||||
self.create_mock_tgz(f'{pkg_name}-{pkg_version}.tgz', {
|
||||
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||
'package/StructureDefinition-us-core-patient.json': {
|
||||
'resourceType': 'StructureDefinition',
|
||||
'snapshot': {'element': [{'path': 'Patient.name', 'min': 1}, {'path': 'Patient.identifier', 'mustSupport': True}]}
|
||||
}
|
||||
})
|
||||
response = self.client.post(
|
||||
'/api/validate-sample',
|
||||
data=json.dumps({
|
||||
'package_name': pkg_name,
|
||||
'version': pkg_version,
|
||||
'sample_data': json.dumps(sample_resource),
|
||||
'mode': 'single',
|
||||
'include_dependencies': True
|
||||
}),
|
||||
content_type='application/json',
|
||||
headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertFalse(data['valid'])
|
||||
self.assertIn('Required element Patient.name missing', data['errors'])
|
||||
self.assertIn('Must Support element Patient.identifier missing', data['warnings'])
|
||||
response = self.client.get('/validate-sample')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'us-core-patient', response.data)
|
||||
|
||||
def test_36_must_support_consistency(self):
|
||||
pkg_name = 'hl7.fhir.us.core'
|
||||
pkg_version = '6.1.0'
|
||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||
self.create_mock_tgz(filename, {
|
||||
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||
'package/StructureDefinition-us-core-patient.json': {
|
||||
'resourceType': 'StructureDefinition',
|
||||
'snapshot': {'element': [{'path': 'Patient.name', 'min': 1}, {'path': 'Patient.identifier', 'mustSupport': True, 'sliceName': 'us-ssn'}]}
|
||||
}
|
||||
})
|
||||
services.process_package_file(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename))
|
||||
sample_resource = {
|
||||
'resourceType': 'Patient',
|
||||
'id': 'test',
|
||||
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']}
|
||||
}
|
||||
response = self.client.post(
|
||||
'/api/validate-sample',
|
||||
data=json.dumps({
|
||||
'package_name': pkg_name,
|
||||
'version': pkg_version,
|
||||
'sample_data': json.dumps(sample_resource),
|
||||
'mode': 'single',
|
||||
'include_dependencies': True
|
||||
}),
|
||||
content_type='application/json',
|
||||
headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('Must Support element Patient.identifier missing', data['warnings'])
|
||||
with self.app_context:
|
||||
ig = ProcessedIg.query.filter_by(package_name=pkg_name, version=pkg_version).first()
|
||||
self.assertIsNotNone(ig)
|
||||
must_support_paths = ig.must_support_elements.get('Patient', [])
|
||||
self.assertIn('Patient.identifier:us-ssn', must_support_paths)
|
||||
response = self.client.get(f'/view-ig/{ig.id}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Patient.identifier:us-ssn', response.data)
|
||||
self.assertIn(b'list-group-item-warning', response.data)
|
||||
|
||||
# --- Existing API Tests ---
|
||||
|
||||
@patch('app.list_downloaded_packages')
|
||||
@patch('app.services.process_package_file')
|
||||
@patch('app.services.import_package_and_dependencies')
|
||||
@patch('os.path.exists')
|
||||
def test_30_api_import_ig_success(self, mock_os_exists, mock_import, mock_process, mock_list_pkgs):
|
||||
pkg_name='api.test.pkg'; pkg_version='1.2.3'; filename=f'{pkg_name}-{pkg_version}.tgz'; pkg_path=os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
def test_40_api_import_ig_success(self, mock_os_exists, mock_import, mock_process, mock_list_pkgs):
|
||||
pkg_name = 'api.test.pkg'
|
||||
pkg_version = '1.2.3'
|
||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||
pkg_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
mock_import.return_value = {'requested': (pkg_name, pkg_version), 'processed': {(pkg_name, pkg_version)}, 'downloaded': {(pkg_name, pkg_version): pkg_path}, 'all_dependencies': {}, 'dependencies': [], 'errors': []}
|
||||
mock_process.return_value = {'resource_types_info': [], 'must_support_elements': {}, 'examples': {}, 'complies_with_profiles': ['http://prof.com/a'], 'imposed_profiles': [], 'errors': []}
|
||||
mock_os_exists.return_value = True
|
||||
mock_list_pkgs.return_value = ([{'name': pkg_name, 'version': pkg_version, 'filename': filename}], [], {})
|
||||
response = self.client.post('/api/import-ig', data=json.dumps({'package_name': pkg_name, 'version': pkg_version, 'dependency_mode': 'direct', 'api_key': 'test-api-key'}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200); data = json.loads(response.data); self.assertEqual(data['status'], 'success'); self.assertEqual(data['complies_with_profiles'], ['http://prof.com/a'])
|
||||
response = self.client.post(
|
||||
'/api/import-ig',
|
||||
data=json.dumps({'package_name': pkg_name, 'version': pkg_version, 'dependency_mode': 'direct', 'api_key': 'test-api-key'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['status'], 'success')
|
||||
self.assertEqual(data['complies_with_profiles'], ['http://prof.com/a'])
|
||||
|
||||
@patch('app.services.import_package_and_dependencies')
|
||||
def test_31_api_import_ig_failure(self, mock_import):
|
||||
def test_41_api_import_ig_failure(self, mock_import):
|
||||
mock_import.return_value = {'requested': ('bad.pkg', '1.0'), 'processed': set(), 'downloaded': {}, 'all_dependencies': {}, 'dependencies': [], 'errors': ['HTTP error: 404 Not Found']}
|
||||
response = self.client.post('/api/import-ig', data=json.dumps({'package_name': 'bad.pkg', 'version': '1.0', 'api_key': 'test-api-key'}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 404); data = json.loads(response.data); self.assertIn('Failed to import bad.pkg#1.0: HTTP error: 404 Not Found', data['message'])
|
||||
response = self.client.post(
|
||||
'/api/import-ig',
|
||||
data=json.dumps({'package_name': 'bad.pkg', 'version': '1.0', 'api_key': 'test-api-key'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('Failed to import bad.pkg#1.0: HTTP error: 404 Not Found', data['message'])
|
||||
|
||||
def test_32_api_import_ig_invalid_key(self):
|
||||
response = self.client.post('/api/import-ig', data=json.dumps({'package_name': 'a', 'version': '1', 'api_key': 'wrong'}), content_type='application/json')
|
||||
def test_42_api_import_ig_invalid_key(self):
|
||||
response = self.client.post(
|
||||
'/api/import-ig',
|
||||
data=json.dumps({'package_name': 'a', 'version': '1', 'api_key': 'wrong'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_33_api_import_ig_missing_key(self):
|
||||
response = self.client.post('/api/import-ig', data=json.dumps({'package_name': 'a', 'version': '1'}), content_type='application/json')
|
||||
def test_43_api_import_ig_missing_key(self):
|
||||
response = self.client.post(
|
||||
'/api/import-ig',
|
||||
data=json.dumps({'package_name': 'a', 'version': '1'}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
|
||||
# --- API Push Tests ---
|
||||
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('app.services.get_package_metadata')
|
||||
@patch('tarfile.open')
|
||||
@patch('requests.Session')
|
||||
def test_40_api_push_ig_success(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||
pkg_name='push.test.pkg'; pkg_version='1.0.0'; filename=f'{pkg_name}-{pkg_version}.tgz'; fhir_server_url='http://fake-fhir.com/baseR4'
|
||||
def test_50_api_push_ig_success(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||
pkg_name = 'push.test.pkg'
|
||||
pkg_version = '1.0.0'
|
||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||
fhir_server_url = 'http://fake-fhir.com/baseR4'
|
||||
mock_get_metadata.return_value = {'imported_dependencies': []}
|
||||
mock_tar = MagicMock(); mock_patient = {'resourceType': 'Patient', 'id': 'pat1'}; mock_obs = {'resourceType': 'Observation', 'id': 'obs1', 'status': 'final'}
|
||||
patient_member = MagicMock(spec=tarfile.TarInfo); patient_member.name = 'package/Patient-pat1.json'; patient_member.isfile.return_value = True
|
||||
obs_member = MagicMock(spec=tarfile.TarInfo); obs_member.name = 'package/Observation-obs1.json'; obs_member.isfile.return_value = True
|
||||
mock_tar = MagicMock()
|
||||
mock_patient = {'resourceType': 'Patient', 'id': 'pat1'}
|
||||
mock_obs = {'resourceType': 'Observation', 'id': 'obs1', 'status': 'final'}
|
||||
patient_member = MagicMock(spec=tarfile.TarInfo)
|
||||
patient_member.name = 'package/Patient-pat1.json'
|
||||
patient_member.isfile.return_value = True
|
||||
obs_member = MagicMock(spec=tarfile.TarInfo)
|
||||
obs_member.name = 'package/Observation-obs1.json'
|
||||
obs_member.isfile.return_value = True
|
||||
mock_tar.getmembers.return_value = [patient_member, obs_member]
|
||||
# FIX: Restore full mock_extractfile definition
|
||||
def mock_extractfile(member):
|
||||
if member.name == 'package/Patient-pat1.json': return io.BytesIO(json.dumps(mock_patient).encode('utf-8'))
|
||||
if member.name == 'package/Observation-obs1.json': return io.BytesIO(json.dumps(mock_obs).encode('utf-8'))
|
||||
if member.name == 'package/Patient-pat1.json':
|
||||
return io.BytesIO(json.dumps(mock_patient).encode('utf-8'))
|
||||
if member.name == 'package/Observation-obs1.json':
|
||||
return io.BytesIO(json.dumps(mock_obs).encode('utf-8'))
|
||||
return None
|
||||
mock_tar.extractfile.side_effect = mock_extractfile
|
||||
mock_tarfile_open.return_value.__enter__.return_value = mock_tar
|
||||
mock_session_instance = MagicMock(); mock_put_response = MagicMock(status_code=200); mock_put_response.raise_for_status.return_value = None; mock_session_instance.put.return_value = mock_put_response; mock_session.return_value = mock_session_instance
|
||||
mock_session_instance = MagicMock()
|
||||
mock_put_response = MagicMock(status_code=200)
|
||||
mock_put_response.raise_for_status.return_value = None
|
||||
mock_session_instance.put.return_value = mock_put_response
|
||||
mock_session.return_value = mock_session_instance
|
||||
self.create_mock_tgz(filename, {'package/dummy.txt': 'content'})
|
||||
response = self.client.post('/api/push-ig', data=json.dumps({'package_name': pkg_name, 'version': pkg_version, 'fhir_server_url': fhir_server_url, 'include_dependencies': False, 'api_key': 'test-api-key'}), content_type='application/json', headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'})
|
||||
self.assertEqual(response.status_code, 200, f"API call failed: {response.status_code} {response.data.decode()}"); self.assertEqual(response.mimetype, 'application/x-ndjson')
|
||||
streamed_data = parse_ndjson(response.data); complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None); self.assertIsNotNone(complete_msg); summary = complete_msg.get('data', {})
|
||||
self.assertEqual(summary.get('status'), 'success'); self.assertEqual(summary.get('success_count'), 2); self.assertEqual(len(summary.get('failed_details')), 0)
|
||||
response = self.client.post(
|
||||
'/api/push-ig',
|
||||
data=json.dumps({
|
||||
'package_name': pkg_name,
|
||||
'version': pkg_version,
|
||||
'fhir_server_url': fhir_server_url,
|
||||
'include_dependencies': False,
|
||||
'api_key': 'test-api-key'
|
||||
}),
|
||||
content_type='application/json',
|
||||
headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.mimetype, 'application/x-ndjson')
|
||||
streamed_data = parse_ndjson(response.data)
|
||||
complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None)
|
||||
self.assertIsNotNone(complete_msg)
|
||||
summary = complete_msg.get('data', {})
|
||||
self.assertEqual(summary.get('status'), 'success')
|
||||
self.assertEqual(summary.get('success_count'), 2)
|
||||
self.assertEqual(len(summary.get('failed_details')), 0)
|
||||
mock_os_exists.assert_called_with(os.path.join(self.test_packages_dir, filename))
|
||||
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('app.services.get_package_metadata')
|
||||
@patch('tarfile.open')
|
||||
@patch('requests.Session')
|
||||
def test_41_api_push_ig_with_failures(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||
pkg_name='push.fail.pkg'; pkg_version='1.0.0'; filename=f'{pkg_name}-{pkg_version}.tgz'; fhir_server_url='http://fail-fhir.com/baseR4'
|
||||
def test_51_api_push_ig_with_failures(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||
pkg_name = 'push.fail.pkg'
|
||||
pkg_version = '1.0.0'
|
||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||
fhir_server_url = 'http://fail-fhir.com/baseR4'
|
||||
mock_get_metadata.return_value = {'imported_dependencies': []}
|
||||
mock_tar = MagicMock(); mock_ok_res = {'resourceType': 'Patient', 'id': 'ok1'}; mock_fail_res = {'resourceType': 'Observation', 'id': 'fail1'}
|
||||
ok_member = MagicMock(spec=tarfile.TarInfo); ok_member.name='package/Patient-ok1.json'; ok_member.isfile.return_value = True
|
||||
fail_member = MagicMock(spec=tarfile.TarInfo); fail_member.name='package/Observation-fail1.json'; fail_member.isfile.return_value = True
|
||||
mock_tar = MagicMock()
|
||||
mock_ok_res = {'resourceType': 'Patient', 'id': 'ok1'}
|
||||
mock_fail_res = {'resourceType': 'Observation', 'id': 'fail1'}
|
||||
ok_member = MagicMock(spec=tarfile.TarInfo)
|
||||
ok_member.name = 'package/Patient-ok1.json'
|
||||
ok_member.isfile.return_value = True
|
||||
fail_member = MagicMock(spec=tarfile.TarInfo)
|
||||
fail_member.name = 'package/Observation-fail1.json'
|
||||
fail_member.isfile.return_value = True
|
||||
mock_tar.getmembers.return_value = [ok_member, fail_member]
|
||||
# FIX: Restore full mock_extractfile definition
|
||||
def mock_extractfile(member):
|
||||
if member.name == 'package/Patient-ok1.json': return io.BytesIO(json.dumps(mock_ok_res).encode('utf-8'))
|
||||
if member.name == 'package/Observation-fail1.json': return io.BytesIO(json.dumps(mock_fail_res).encode('utf-8'))
|
||||
if member.name == 'package/Patient-ok1.json':
|
||||
return io.BytesIO(json.dumps(mock_ok_res).encode('utf-8'))
|
||||
if member.name == 'package/Observation-fail1.json':
|
||||
return io.BytesIO(json.dumps(mock_fail_res).encode('utf-8'))
|
||||
return None
|
||||
mock_tar.extractfile.side_effect = mock_extractfile
|
||||
mock_tarfile_open.return_value.__enter__.return_value = mock_tar
|
||||
mock_session_instance = MagicMock(); mock_ok_response = MagicMock(status_code=200); mock_ok_response.raise_for_status.return_value = None
|
||||
mock_fail_http_response = MagicMock(status_code=400); mock_fail_http_response.json.return_value = {'resourceType': 'OperationOutcome', 'issue': [{'severity': 'error', 'diagnostics': 'Validation failed'}]}; mock_fail_exception = requests.exceptions.HTTPError(response=mock_fail_http_response); mock_fail_http_response.raise_for_status.side_effect = mock_fail_exception
|
||||
mock_session_instance.put.side_effect = [mock_ok_response, mock_fail_http_response]; mock_session.return_value = mock_session_instance
|
||||
mock_session_instance = MagicMock()
|
||||
mock_ok_response = MagicMock(status_code=200)
|
||||
mock_ok_response.raise_for_status.return_value = None
|
||||
mock_fail_http_response = MagicMock(status_code=400)
|
||||
mock_fail_http_response.json.return_value = {'resourceType': 'OperationOutcome', 'issue': [{'severity': 'error', 'diagnostics': 'Validation failed'}]}
|
||||
mock_fail_exception = requests.exceptions.HTTPError(response=mock_fail_http_response)
|
||||
mock_fail_http_response.raise_for_status.side_effect = mock_fail_exception
|
||||
mock_session_instance.put.side_effect = [mock_ok_response, mock_fail_http_response]
|
||||
mock_session.return_value = mock_session_instance
|
||||
self.create_mock_tgz(filename, {'package/dummy.txt': 'content'})
|
||||
response = self.client.post('/api/push-ig', data=json.dumps({'package_name': pkg_name, 'version': pkg_version, 'fhir_server_url': fhir_server_url, 'include_dependencies': False, 'api_key': 'test-api-key'}), content_type='application/json', headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'})
|
||||
self.assertEqual(response.status_code, 200, f"API call failed: {response.status_code} {response.data.decode()}"); streamed_data = parse_ndjson(response.data); complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None); self.assertIsNotNone(complete_msg); summary = complete_msg.get('data', {})
|
||||
self.assertEqual(summary.get('status'), 'partial'); self.assertEqual(summary.get('success_count'), 1); self.assertEqual(summary.get('failure_count'), 1); self.assertEqual(len(summary.get('failed_details')), 1)
|
||||
self.assertEqual(summary['failed_details'][0].get('resource'), 'Observation/fail1'); self.assertIn('Validation failed', summary['failed_details'][0].get('error', ''))
|
||||
response = self.client.post(
|
||||
'/api/push-ig',
|
||||
data=json.dumps({
|
||||
'package_name': pkg_name,
|
||||
'version': pkg_version,
|
||||
'fhir_server_url': fhir_server_url,
|
||||
'include_dependencies': False,
|
||||
'api_key': 'test-api-key'
|
||||
}),
|
||||
content_type='application/json',
|
||||
headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
streamed_data = parse_ndjson(response.data)
|
||||
complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None)
|
||||
self.assertIsNotNone(complete_msg)
|
||||
summary = complete_msg.get('data', {})
|
||||
self.assertEqual(summary.get('status'), 'partial')
|
||||
self.assertEqual(summary.get('success_count'), 1)
|
||||
self.assertEqual(summary.get('failure_count'), 1)
|
||||
self.assertEqual(len(summary.get('failed_details')), 1)
|
||||
self.assertEqual(summary['failed_details'][0].get('resource'), 'Observation/fail1')
|
||||
self.assertIn('Validation failed', summary['failed_details'][0].get('error', ''))
|
||||
mock_os_exists.assert_called_with(os.path.join(self.test_packages_dir, filename))
|
||||
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('app.services.get_package_metadata')
|
||||
@patch('tarfile.open')
|
||||
@patch('requests.Session')
|
||||
def test_42_api_push_ig_with_dependency(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||
main_pkg_name='main.dep.pkg'; main_pkg_ver='1.0'; main_filename=f'{main_pkg_name}-{main_pkg_ver}.tgz'; dep_pkg_name='dep.pkg'; dep_pkg_ver='1.0'; dep_filename=f'{dep_pkg_name}-{dep_pkg_ver}.tgz'; fhir_server_url='http://dep-fhir.com/baseR4'
|
||||
def test_52_api_push_ig_with_dependency(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||
main_pkg_name = 'main.dep.pkg'
|
||||
main_pkg_ver = '1.0'
|
||||
main_filename = f'{main_pkg_name}-{main_pkg_ver}.tgz'
|
||||
dep_pkg_name = 'dep.pkg'
|
||||
dep_pkg_ver = '1.0'
|
||||
dep_filename = f'{dep_pkg_name}-{dep_pkg_ver}.tgz'
|
||||
fhir_server_url = 'http://dep-fhir.com/baseR4'
|
||||
self.create_mock_tgz(main_filename, {'package/Patient-main.json': {'resourceType': 'Patient', 'id': 'main'}})
|
||||
self.create_mock_tgz(dep_filename, {'package/Observation-dep.json': {'resourceType': 'Observation', 'id': 'dep'}})
|
||||
mock_get_metadata.return_value = { 'package_name': main_pkg_name, 'version': main_pkg_ver, 'dependency_mode': 'recursive', 'imported_dependencies': [{'name': dep_pkg_name, 'version': dep_pkg_ver}]}
|
||||
mock_main_tar = MagicMock(); main_member = MagicMock(spec=tarfile.TarInfo); main_member.name='package/Patient-main.json'; main_member.isfile.return_value = True; mock_main_tar.getmembers.return_value = [main_member]; mock_main_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'Patient', 'id': 'main'}).encode('utf-8'))
|
||||
mock_dep_tar = MagicMock(); dep_member = MagicMock(spec=tarfile.TarInfo); dep_member.name='package/Observation-dep.json'; dep_member.isfile.return_value = True; mock_dep_tar.getmembers.return_value = [dep_member]; mock_dep_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'Observation', 'id': 'dep'}).encode('utf-8'))
|
||||
# FIX: Restore full tar_opener definition
|
||||
mock_get_metadata.return_value = {'imported_dependencies': [{'name': dep_pkg_name, 'version': dep_pkg_ver}]}
|
||||
mock_main_tar = MagicMock()
|
||||
main_member = MagicMock(spec=tarfile.TarInfo)
|
||||
main_member.name = 'package/Patient-main.json'
|
||||
main_member.isfile.return_value = True
|
||||
mock_main_tar.getmembers.return_value = [main_member]
|
||||
mock_main_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'Patient', 'id': 'main'}).encode('utf-8'))
|
||||
mock_dep_tar = MagicMock()
|
||||
dep_member = MagicMock(spec=tarfile.TarInfo)
|
||||
dep_member.name = 'package/Observation-dep.json'
|
||||
dep_member.isfile.return_value = True
|
||||
mock_dep_tar.getmembers.return_value = [dep_member]
|
||||
mock_dep_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'Observation', 'id': 'dep'}).encode('utf-8'))
|
||||
def tar_opener(path, mode):
|
||||
mock_tar_ctx = MagicMock()
|
||||
if main_filename in path: mock_tar_ctx.__enter__.return_value = mock_main_tar
|
||||
elif dep_filename in path: mock_tar_ctx.__enter__.return_value = mock_dep_tar
|
||||
else: empty_mock_tar = MagicMock(); empty_mock_tar.getmembers.return_value = []; mock_tar_ctx.__enter__.return_value = empty_mock_tar
|
||||
if main_filename in path:
|
||||
mock_tar_ctx.__enter__.return_value = mock_main_tar
|
||||
elif dep_filename in path:
|
||||
mock_tar_ctx.__enter__.return_value = mock_dep_tar
|
||||
else:
|
||||
empty_mock_tar = MagicMock()
|
||||
empty_mock_tar.getmembers.return_value = []
|
||||
mock_tar_ctx.__enter__.return_value = empty_mock_tar
|
||||
return mock_tar_ctx
|
||||
mock_tarfile_open.side_effect = tar_opener
|
||||
mock_session_instance = MagicMock(); mock_put_response = MagicMock(status_code=200); mock_put_response.raise_for_status.return_value = None; mock_session_instance.put.return_value = mock_put_response; mock_session.return_value = mock_session_instance
|
||||
response = self.client.post('/api/push-ig', data=json.dumps({'package_name': main_pkg_name, 'version': main_pkg_ver, 'fhir_server_url': fhir_server_url, 'include_dependencies': True, 'api_key': 'test-api-key'}), content_type='application/json', headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'})
|
||||
self.assertEqual(response.status_code, 200, f"API call failed: {response.status_code} {response.data.decode()}"); streamed_data = parse_ndjson(response.data); complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None); self.assertIsNotNone(complete_msg); summary = complete_msg.get('data', {})
|
||||
self.assertEqual(summary.get('status'), 'success'); self.assertEqual(summary.get('success_count'), 2); self.assertEqual(len(summary.get('pushed_packages_summary')), 2)
|
||||
self.assertGreaterEqual(mock_os_exists.call_count, 2); mock_os_exists.assert_any_call(os.path.join(self.test_packages_dir, main_filename)); mock_os_exists.assert_any_call(os.path.join(self.test_packages_dir, dep_filename))
|
||||
mock_session_instance = MagicMock()
|
||||
mock_put_response = MagicMock(status_code=200)
|
||||
mock_put_response.raise_for_status.return_value = None
|
||||
mock_session_instance.put.return_value = mock_put_response
|
||||
mock_session.return_value = mock_session_instance
|
||||
response = self.client.post(
|
||||
'/api/push-ig',
|
||||
data=json.dumps({
|
||||
'package_name': main_pkg_name,
|
||||
'version': main_pkg_ver,
|
||||
'fhir_server_url': fhir_server_url,
|
||||
'include_dependencies': True,
|
||||
'api_key': 'test-api-key'
|
||||
}),
|
||||
content_type='application/json',
|
||||
headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
streamed_data = parse_ndjson(response.data)
|
||||
complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None)
|
||||
self.assertIsNotNone(complete_msg)
|
||||
summary = complete_msg.get('data', {})
|
||||
self.assertEqual(summary.get('status'), 'success')
|
||||
self.assertEqual(summary.get('success_count'), 2)
|
||||
self.assertEqual(len(summary.get('pushed_packages_summary')), 2)
|
||||
mock_os_exists.assert_any_call(os.path.join(self.test_packages_dir, main_filename))
|
||||
mock_os_exists.assert_any_call(os.path.join(self.test_packages_dir, dep_filename))
|
||||
|
||||
# --- Helper Route Tests ---
|
||||
|
||||
@patch('app.ProcessedIg.query')
|
||||
@patch('app.services.find_and_extract_sd')
|
||||
@patch('os.path.exists')
|
||||
def test_50_get_structure_definition_success(self, mock_exists, mock_find_sd, mock_query):
|
||||
pkg_name='struct.test'; pkg_version='1.0'; resource_type='Patient'; mock_exists.return_value = True
|
||||
def test_60_get_structure_definition_success(self, mock_exists, mock_find_sd, mock_query):
|
||||
pkg_name = 'struct.test'
|
||||
pkg_version = '1.0'
|
||||
resource_type = 'Patient'
|
||||
mock_exists.return_value = True
|
||||
mock_sd_data = {'resourceType': 'StructureDefinition', 'snapshot': {'element': [{'id': 'Patient.name', 'min': 1}, {'id': 'Patient.birthDate', 'mustSupport': True}]}}
|
||||
mock_find_sd.return_value = (mock_sd_data, 'path/to/sd.json')
|
||||
mock_processed_ig = MagicMock(); mock_processed_ig.must_support_elements = {resource_type: ['Patient.birthDate']}; mock_query.filter_by.return_value.first.return_value = mock_processed_ig
|
||||
mock_processed_ig = MagicMock()
|
||||
mock_processed_ig.must_support_elements = {resource_type: ['Patient.birthDate']}
|
||||
mock_query.filter_by.return_value.first.return_value = mock_processed_ig
|
||||
response = self.client.get(f'/get-structure?package_name={pkg_name}&package_version={pkg_version}&resource_type={resource_type}')
|
||||
self.assertEqual(response.status_code, 200); data = json.loads(response.data); self.assertEqual(data['must_support_paths'], ['Patient.birthDate'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['must_support_paths'], ['Patient.birthDate'])
|
||||
|
||||
@patch('app.services.import_package_and_dependencies')
|
||||
@patch('app.services.find_and_extract_sd')
|
||||
@patch('os.path.exists')
|
||||
def test_51_get_structure_definition_fallback(self, mock_exists, mock_find_sd, mock_import):
|
||||
pkg_name='struct.test'; pkg_version='1.0'; core_pkg_name, core_pkg_version = services.CANONICAL_PACKAGE; resource_type='Observation'
|
||||
def exists_side_effect(path): return True; mock_exists.side_effect = exists_side_effect
|
||||
def test_61_get_structure_definition_fallback(self, mock_exists, mock_find_sd, mock_import):
|
||||
pkg_name = 'struct.test'
|
||||
pkg_version = '1.0'
|
||||
core_pkg_name, core_pkg_version = services.CANONICAL_PACKAGE
|
||||
resource_type = 'Observation'
|
||||
def exists_side_effect(path):
|
||||
return True
|
||||
mock_exists.side_effect = exists_side_effect
|
||||
mock_core_sd_data = {'resourceType': 'StructureDefinition', 'snapshot': {'element': [{'id': 'Observation.status'}]}}
|
||||
def find_sd_side_effect(path, identifier, profile_url=None):
|
||||
if f"{pkg_name}-{pkg_version}.tgz" in path: return (None, None)
|
||||
if f"{core_pkg_name}-{core_pkg_version}.tgz" in path: return (mock_core_sd_data, 'path/obs.json')
|
||||
if f"{pkg_name}-{pkg_version}.tgz" in path:
|
||||
return (None, None)
|
||||
if f"{core_pkg_name}-{core_pkg_version}.tgz" in path:
|
||||
return (mock_core_sd_data, 'path/obs.json')
|
||||
return (None, None)
|
||||
mock_find_sd.side_effect = find_sd_side_effect
|
||||
with patch('app.ProcessedIg.query') as mock_query:
|
||||
mock_query.filter_by.return_value.first.return_value = None
|
||||
response = self.client.get(f'/get-structure?package_name={pkg_name}&package_version={pkg_version}&resource_type={resource_type}')
|
||||
self.assertEqual(response.status_code, 200); data = json.loads(response.data); self.assertTrue(data['fallback_used'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertTrue(data['fallback_used'])
|
||||
|
||||
@patch('app.services.find_and_extract_sd', return_value=(None, None))
|
||||
@patch('app.services.import_package_and_dependencies')
|
||||
@patch('os.path.exists')
|
||||
def test_52_get_structure_definition_not_found_anywhere(self, mock_exists, mock_import, mock_find_sd):
|
||||
pkg_name = 'no.sd.pkg'; pkg_version = '1.0'; core_pkg_name, core_pkg_version = services.CANONICAL_PACKAGE
|
||||
def test_62_get_structure_definition_not_found_anywhere(self, mock_exists, mock_import, mock_find_sd):
|
||||
pkg_name = 'no.sd.pkg'
|
||||
pkg_version = '1.0'
|
||||
core_pkg_name, core_pkg_version = services.CANONICAL_PACKAGE
|
||||
def exists_side_effect(path):
|
||||
if f"{pkg_name}-{pkg_version}.tgz" in path: return True
|
||||
if f"{core_pkg_name}-{core_pkg_version}.tgz" in path: return False
|
||||
if f"{pkg_name}-{pkg_version}.tgz" in path:
|
||||
return True
|
||||
if f"{core_pkg_name}-{core_pkg_version}.tgz" in path:
|
||||
return False
|
||||
return False
|
||||
mock_exists.side_effect = exists_side_effect
|
||||
mock_import.return_value = {'errors': ['Download failed'], 'downloaded': False}
|
||||
response = self.client.get(f'/get-structure?package_name={pkg_name}&package_version={pkg_version}&resource_type=Whatever')
|
||||
self.assertEqual(response.status_code, 500); data = json.loads(response.data); self.assertIn('failed to download core package', data['error'])
|
||||
self.assertEqual(response.status_code, 500)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('failed to download core package', data['error'])
|
||||
|
||||
def test_53_get_example_content_success(self):
|
||||
pkg_name = 'example.test'; pkg_version = '1.0'; filename = f"{pkg_name}-{pkg_version}.tgz"
|
||||
example_path = 'package/Patient-example.json'; example_content = {'resourceType': 'Patient', 'id': 'example'}
|
||||
def test_63_get_example_content_success(self):
|
||||
pkg_name = 'example.test'
|
||||
pkg_version = '1.0'
|
||||
filename = f"{pkg_name}-{pkg_version}.tgz"
|
||||
example_path = 'package/Patient-example.json'
|
||||
example_content = {'resourceType': 'Patient', 'id': 'example'}
|
||||
self.create_mock_tgz(filename, {example_path: example_content})
|
||||
response = self.client.get(f'/get-example?package_name={pkg_name}&package_version={pkg_version}&filename={example_path}')
|
||||
self.assertEqual(response.status_code, 200); data = json.loads(response.data); self.assertEqual(data, example_content)
|
||||
|
||||
def test_54_get_package_metadata_success(self):
|
||||
pkg_name = 'metadata.test'; pkg_version = '1.0'; metadata_filename = f"{pkg_name}-{pkg_version}.metadata.json"
|
||||
metadata_content = {'package_name': pkg_name, 'version': pkg_version, 'dependency_mode': 'tree-shaking'}
|
||||
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename); open(metadata_path, 'w').write(json.dumps(metadata_content))
|
||||
response = self.client.get(f'/get-package-metadata?package_name={pkg_name}&version={pkg_version}')
|
||||
self.assertEqual(response.status_code, 200); data = json.loads(response.data); self.assertEqual(data.get('dependency_mode'), 'tree-shaking')
|
||||
|
||||
|
||||
# --- Validation API Tests --- (/api/validate-sample) ---
|
||||
|
||||
# FIX: Use patch.object decorator targeting the imported services module
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch.object(services, 'validate_resource_against_profile')
|
||||
def test_60_api_validate_sample_single_success(self, mock_validate, mock_os_exists): # Note order change
|
||||
pkg_name='validate.pkg'; pkg_version='1.0'
|
||||
mock_validate.return_value = {'valid': True, 'errors': [], 'warnings': [], 'details': [], 'resource_type': 'Patient', 'resource_id': 'valid1', 'profile': 'P', 'summary': {'error_count': 0, 'warning_count': 0}}
|
||||
sample_resource = {'resourceType': 'Patient', 'id': 'valid1', 'meta': {'profile': ['P']}}
|
||||
response = self.client.post('/api/validate-sample',
|
||||
data=json.dumps({'package_name': pkg_name, 'version': pkg_version, 'sample_data': json.dumps(sample_resource), 'mode': 'single', 'include_dependencies': True }),
|
||||
content_type='application/json', headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data); self.assertTrue(data['valid'])
|
||||
mock_validate.assert_called_once_with(pkg_name, pkg_version, sample_resource, include_dependencies=True)
|
||||
|
||||
# FIX: Use patch.object decorator
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch.object(services, 'validate_resource_against_profile')
|
||||
def test_61_api_validate_sample_single_failure(self, mock_validate, mock_os_exists): # Note order change
|
||||
pkg_name='validate.pkg'; pkg_version='1.0'
|
||||
mock_validate.return_value = {'valid': False, 'errors': ['E1'], 'warnings': ['W1'], 'details': [], 'resource_type': 'Patient', 'resource_id': 'invalid1', 'profile': 'P', 'summary': {'error_count': 1, 'warning_count': 1}}
|
||||
sample_resource = {'resourceType': 'Patient', 'id': 'invalid1', 'meta': {'profile': ['P']}}
|
||||
response = self.client.post('/api/validate-sample',
|
||||
data=json.dumps({'package_name': pkg_name, 'version': pkg_version, 'sample_data': json.dumps(sample_resource), 'mode': 'single', 'include_dependencies': False }),
|
||||
content_type='application/json', headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data); self.assertFalse(data['valid'])
|
||||
mock_validate.assert_called_once_with(pkg_name, pkg_version, sample_resource, include_dependencies=False)
|
||||
|
||||
# FIX: Use patch.object decorator
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch.object(services, 'validate_bundle_against_profile')
|
||||
def test_62_api_validate_sample_bundle_success(self, mock_validate_bundle, mock_os_exists): # Note order change
|
||||
pkg_name='validate.pkg'; pkg_version='1.0'
|
||||
mock_validate_bundle.return_value = { 'valid': True, 'errors': [], 'warnings': [], 'details': [], 'results': {'Patient/p1': {'valid': True, 'errors': [], 'warnings': []}}, 'summary': {'resource_count': 1, 'failed_resources': 0, 'profiles_validated': ['P'], 'error_count': 0, 'warning_count': 0} }
|
||||
sample_bundle = {'resourceType': 'Bundle', 'type': 'collection', 'entry': [{'resource': {'resourceType': 'Patient', 'id': 'p1'}}]}
|
||||
response = self.client.post('/api/validate-sample',
|
||||
data=json.dumps({'package_name': pkg_name, 'version': pkg_version, 'sample_data': json.dumps(sample_bundle), 'mode': 'bundle', 'include_dependencies': True }),
|
||||
content_type='application/json', headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data); self.assertTrue(data['valid'])
|
||||
mock_validate_bundle.assert_called_once_with(pkg_name, pkg_version, sample_bundle, include_dependencies=True)
|
||||
|
||||
def test_63_api_validate_sample_invalid_json(self):
|
||||
pkg_name = 'p'; pkg_version = '1'
|
||||
self.create_mock_tgz(f"{pkg_name}-{pkg_version}.tgz", {'package/dummy.txt': 'content'})
|
||||
response = self.client.post('/api/validate-sample',
|
||||
data=json.dumps({ 'package_name': pkg_name, 'version': pkg_version, 'sample_data': '{"key": "value", invalid}', 'mode': 'single', 'include_dependencies': True }),
|
||||
content_type='application/json', headers={'X-API-Key': 'test-api-key'}
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
data = json.loads(response.data)
|
||||
self.assertIn('Invalid JSON', data.get('errors', [''])[0])
|
||||
self.assertEqual(data, example_content)
|
||||
|
||||
def test_64_get_package_metadata_success(self):
|
||||
pkg_name = 'metadata.test'
|
||||
pkg_version = '1.0'
|
||||
metadata_filename = f"{pkg_name}-{pkg_version}.metadata.json"
|
||||
metadata_content = {'package_name': pkg_name, 'version': pkg_version, 'dependency_mode': 'tree-shaking'}
|
||||
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename)
|
||||
open(metadata_path, 'w').write(json.dumps(metadata_content))
|
||||
response = self.client.get(f'/get-package-metadata?package_name={pkg_name}&version={pkg_version}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data.get('dependency_mode'), 'tree-shaking')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user