Compare commits

..

No commits in common. "main" and "V4-Alpha-preview" have entirely different histories.

17 changed files with 827 additions and 1770 deletions

View File

@ -2,130 +2,57 @@
setlocal enabledelayedexpansion
REM --- Configuration ---
set REPO_URL_HAPI=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
set REPO_URL_CANDLE=https://github.com/FHIR/fhir-candle.git
set CLONE_DIR_HAPI=hapi-fhir-jpaserver
set CLONE_DIR_CANDLE=fhir-candle
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_HAPI%\target\classes\%CONFIG_FILE%
set DEST_CONFIG_PATH=%CLONE_DIR%\target\classes\%CONFIG_FILE%
REM --- NEW: Define a variable for the custom FHIR URL and server type ---
set "CUSTOM_FHIR_URL_VAL="
set "SERVER_TYPE="
set "CANDLE_FHIR_VERSION="
REM === MODIFIED: Prompt for Installation Mode ===
REM === CORRECTED: Prompt for Version ===
:GetModeChoice
SET "APP_MODE=" REM Clear the variable first
echo.
echo Select Installation Mode:
echo 1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet needed)
echo 2. Custom URL (Uses a custom FHIR Server - No Git/Maven/Dotnet needed)
echo 3. Hapi (Includes local HAPI FHIR Server - Requires Git & Maven)
echo 4. Candle (Includes local FHIR Candle Server - Requires Git & Dotnet)
CHOICE /C 1234 /N /M "Enter your choice (1, 2, 3, or 4):"
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 4 (
SET APP_MODE=standalone
SET SERVER_TYPE=candle
goto :GetCandleFhirVersion
)
IF ERRORLEVEL 3 (
SET APP_MODE=standalone
SET SERVER_TYPE=hapi
goto :ModeSet
)
IF ERRORLEVEL 2 (
SET APP_MODE=standalone
goto :GetCustomUrl
)
IF ERRORLEVEL 1 (
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
:GetCustomUrl
set "CONFIRMED_URL="
:PromptUrlLoop
echo.
set /p "CUSTOM_URL_INPUT=Please enter the custom FHIR server URL: "
echo.
echo You entered: !CUSTOM_URL_INPUT!
set /p "CONFIRM_URL=Is this URL correct? (Y/N): "
if /i "!CONFIRM_URL!" EQU "Y" (
set "CONFIRMED_URL=!CUSTOM_URL_INPUT!"
goto :ConfirmUrlLoop
) else (
goto :PromptUrlLoop
)
:ConfirmUrlLoop
echo.
echo Please re-enter the URL to confirm it is correct:
set /p "CUSTOM_URL_INPUT=Re-enter URL: "
if /i "!CUSTOM_URL_INPUT!" EQU "!CONFIRMED_URL!" (
set "CUSTOM_FHIR_URL_VAL=!CUSTOM_URL_INPUT!"
echo.
echo Custom URL confirmed: !CUSTOM_FHIR_URL_VAL!
goto :ModeSet
) else (
echo.
echo URLs do not match. Please try again.
goto :PromptUrlLoop
)
:GetCandleFhirVersion
echo.
echo Select the FHIR version for the Candle server:
echo 1. R4 (4.0)
echo 2. R4B (4.3)
echo 3. R5 (5.0)
CHOICE /C 123 /N /M "Enter your choice (1, 2, or 3):"
IF ERRORLEVEL 3 (
SET CANDLE_FHIR_VERSION=r5
goto :ModeSet
)
IF ERRORLEVEL 2 (
SET CANDLE_FHIR_VERSION=r4b
goto :ModeSet
)
IF ERRORLEVEL 1 (
SET CANDLE_FHIR_VERSION=r4
goto :ModeSet
)
echo Invalid input. Please try again.
goto :GetCandleFhirVersion
:ModeSet
IF "%APP_MODE%"=="" (
echo Invalid choice detected after checks. Exiting.
goto :eof
)
echo Selected Mode: %APP_MODE%
echo Server Type: %SERVER_TYPE%
echo.
REM === END MODIFICATION ===
REM === END CORRECTION ===
REM === Conditionally Execute Server Setup ===
IF "%SERVER_TYPE%"=="hapi" (
echo Running Hapi server setup...
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_HAPI%
if exist "%CLONE_DIR_HAPI%" (
echo Checking for existing directory: %CLONE_DIR%
if exist "%CLONE_DIR%" (
echo Found existing directory, removing it...
rmdir /s /q "%CLONE_DIR_HAPI%"
rmdir /s /q "%CLONE_DIR%"
if errorlevel 1 (
echo ERROR: Failed to remove existing directory: %CLONE_DIR_HAPI%
echo ERROR: Failed to remove existing directory: %CLONE_DIR%
goto :error
)
echo Existing directory removed.
@ -135,8 +62,8 @@ IF "%SERVER_TYPE%"=="hapi" (
echo.
REM --- Step 1: Clone the HAPI FHIR server repository ---
echo Cloning repository: %REPO_URL_HAPI% into %CLONE_DIR_HAPI%...
git clone "%REPO_URL_HAPI%" "%CLONE_DIR_HAPI%"
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
@ -145,10 +72,10 @@ IF "%SERVER_TYPE%"=="hapi" (
echo.
REM --- Step 2: Navigate into the cloned directory ---
echo Changing directory to %CLONE_DIR_HAPI%...
cd "%CLONE_DIR_HAPI%"
echo Changing directory to %CLONE_DIR%...
cd "%CLONE_DIR%"
if errorlevel 1 (
echo ERROR: Failed to change directory to %CLONE_DIR_HAPI%.
echo ERROR: Failed to change directory to %CLONE_DIR%.
goto :error
)
echo Current directory: %CD%
@ -165,7 +92,7 @@ IF "%SERVER_TYPE%"=="hapi" (
)
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...
@ -191,141 +118,47 @@ IF "%SERVER_TYPE%"=="hapi" (
echo Current directory: %CD%
echo.
) ELSE IF "%SERVER_TYPE%"=="candle" (
echo Running FHIR Candle server setup...
echo.
REM --- Step 0: Clean up previous clone (optional) ---
echo Checking for existing directory: %CLONE_DIR_CANDLE%
if exist "%CLONE_DIR_CANDLE%" (
echo Found existing directory, removing it...
rmdir /s /q "%CLONE_DIR_CANDLE%"
if errorlevel 1 (
echo ERROR: Failed to remove existing directory: %CLONE_DIR_CANDLE%
goto :error
)
echo Existing directory removed.
) else (
echo Directory does not exist, proceeding with clone.
)
echo.
REM --- Step 1: Clone the FHIR Candle server repository ---
echo Cloning repository: %REPO_URL_CANDLE% into %CLONE_DIR_CANDLE%...
git clone "%REPO_URL_CANDLE%" "%CLONE_DIR_CANDLE%"
if errorlevel 1 (
echo ERROR: Failed to clone repository. Check Git and Dotnet 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_CANDLE%...
cd "%CLONE_DIR_CANDLE%"
if errorlevel 1 (
echo ERROR: Failed to change directory to %CLONE_DIR_CANDLE%.
goto :error
)
echo Current directory: %CD%
echo.
REM --- Step 3: Build the FHIR Candle server using Dotnet ---
echo ===> "Starting Dotnet build (Step 3)...""
dotnet publish -c Release -f net9.0 -o publish
echo ===> Dotnet command finished. Checking error level...
if errorlevel 1 (
echo ERROR: Dotnet build failed. Check Dotnet SDK installation.
cd ..
goto :error
)
echo Dotnet build completed successfully. ErrorLevel: %errorlevel%
echo.
REM --- Step 4: Navigate back to the parent directory ---
echo ===> "Changing directory back (Step 4)..."
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 server build...
REM Ensure the server directories don't exist in Lite mode
if exist "%CLONE_DIR_HAPI%" (
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_HAPI%"
rmdir /s /q "%CLONE_DIR%"
)
if exist "%CLONE_DIR_CANDLE%" (
echo Found existing Candle directory in Lite mode. Removing it to avoid build issues...
rmdir /s /q "%CLONE_DIR_CANDLE%"
)
REM Create empty placeholder files to satisfy Dockerfile COPY commands in Lite mode.
mkdir "%CLONE_DIR_HAPI%\target\classes" 2> nul
mkdir "%CLONE_DIR_HAPI%\custom" 2> nul
echo. > "%CLONE_DIR_HAPI%\target\ROOT.war"
echo. > "%CLONE_DIR_HAPI%\target\classes\application.yaml"
mkdir "%CLONE_DIR_CANDLE%\publish" 2> nul
echo. > "%CLONE_DIR_CANDLE%\publish\fhir-candle.dll"
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 === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL ===
echo Updating docker-compose.yml with APP_MODE=%APP_MODE% and HAPI_FHIR_URL...
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: .
IF "%SERVER_TYPE%"=="hapi" (
echo dockerfile: Dockerfile.hapi
) ELSE IF "%SERVER_TYPE%"=="candle" (
echo dockerfile: Dockerfile.candle
) ELSE (
echo dockerfile: Dockerfile.lite
)
echo dockerfile: Dockerfile
echo ports:
IF "%SERVER_TYPE%"=="candle" (
echo - "5000:5000"
echo - "5001:5826"
) ELSE (
echo - "5000:5000"
echo - "8080:8080"
)
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
IF "%SERVER_TYPE%"=="hapi" (
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
)
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
echo - ./logs:/app/logs
IF "%SERVER_TYPE%"=="hapi" (
echo - ./hapi-fhir-jpaserver/target/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
echo - ./hapi-fhir-jpaserver/target/classes/application.yaml:/usr/local/tomcat/conf/application.yaml
) ELSE IF "%SERVER_TYPE%"=="candle" (
echo - ./fhir-candle/publish/:/app/fhir-candle-publish/
)
echo environment:
echo - FLASK_APP=app.py
echo - FLASK_ENV=development
echo - NODE_PATH=/usr/lib/node_modules
echo - APP_MODE=%APP_MODE%
echo - APP_BASE_URL=http://localhost:5000
IF DEFINED CUSTOM_FHIR_URL_VAL (
echo - HAPI_FHIR_URL=!CUSTOM_FHIR_URL_VAL!
) ELSE (
IF "%SERVER_TYPE%"=="candle" (
echo - HAPI_FHIR_URL=http://localhost:5826/fhir/%CANDLE_FHIR_VERSION%
echo - ASPNETCORE_URLS=http://0.0.0.0:5826
) ELSE (
echo - HAPI_FHIR_URL=http://localhost:8080/fhir
)
)
echo - HAPI_FHIR_URL=http://localhost:8080/fhir
echo command: supervisord -c /etc/supervisord.conf
) > docker-compose.yml.tmp
@ -375,4 +208,4 @@ exit /b 1
:eof
echo Script execution finished.
pause
pause

View File

@ -3,16 +3,13 @@ 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 git \
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/*
# ADDED: Install the Dotnet SDK for the FHIR Candle server
# This makes the image a universal base for either server type
RUN apt-get update && apt-get install -y dotnet-sdk-6.0
# Install specific versions of GoFSH and SUSHI
# REMOVED pip install fhirpath from this line
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
@ -27,6 +24,7 @@ RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
WORKDIR /app
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
@ -57,9 +55,6 @@ COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application
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/
# ADDED: Copy pre-built Candle DLL files
COPY fhir-candle/publish/ /app/fhir-candle-publish/
# Install supervisord
RUN pip install supervisor
@ -67,7 +62,7 @@ RUN pip install supervisor
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000 8080 5001
EXPOSE 5000 8080
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

View File

@ -1,64 +0,0 @@
# Base image with Python and Dotnet
FROM mcr.microsoft.com/dotnet/sdk:9.0
# 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 \
default-jre-headless \
curl coreutils git \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install specific versions of GoFSH and SUSHI
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
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 .
COPY package.py .
COPY templates/ templates/
COPY static/ static/
COPY tests/ tests/
# 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
# Copy pre-built Candle DLL files
COPY fhir-candle/publish/ /app/fhir-candle-publish/
# Install supervisord
RUN pip install supervisor
# Configure supervisord
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000 5001
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

View File

@ -1,66 +0,0 @@
# Base image with Python, Java, and Maven
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 git maven \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install specific versions of GoFSH and SUSHI
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
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 .
COPY package.py .
COPY templates/ templates/
COPY static/ static/
COPY tests/ tests/
# 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
# 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"]

View File

@ -1,60 +0,0 @@
# Base image with Python and Node.js
FROM python:3.9-slim
# Install JRE and other dependencies for the validator
# We need to install `default-jre-headless` to get a minimal Java Runtime Environment
RUN apt-get update && apt-get install -y --no-install-recommends \
default-jre-headless \
curl coreutils git \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install specific versions of GoFSH and SUSHI
RUN npm install -g gofsh fsh-sushi
# ADDED: Download the latest HL7 FHIR Validator CLI
RUN mkdir -p /app/validator_cli
WORKDIR /app/validator_cli
# Download the validator JAR and a separate checksum file for verification
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
# Set permissions for the downloaded file
RUN chmod 755 validator_cli.jar
# Change back to the main app directory for the next steps
WORKDIR /app
# Set up Python environment
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 .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY services.py .
COPY forms.py .
COPY package.py .
COPY templates/ templates/
COPY static/ static/
COPY tests/ tests/
# Ensure necessary directories are writable
RUN mkdir -p /app/static/uploads /app/logs && chmod 777 /app/static/uploads /app/logs
# Install supervisord
RUN pip install supervisor
# Configure supervisord
COPY supervisord.conf /etc/supervisord.conf
# Expose ports
EXPOSE 5000
# Start supervisord
CMD ["supervisord", "-c", "/etc/supervisord.conf"]

View File

@ -5,65 +5,22 @@
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs) and test data. It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, converting FHIR resources to FHIR Shorthand (FSH), uploading complex test data sets with dependency management, and retrieving/splitting FHIR bundles. The toolkit includes live consoles for real-time feedback, making it an essential tool for FHIR developers and implementers.
The application can run in four modes:
The application can run in two modes:
* Installation Modes (Lite, Custom URL, Hapi, and Candle)
This toolkit now offers four primary installation modes, configured via a simple command-line prompt during setup:
Lite Mode
Includes the Flask frontend, SQLite database, and core tooling (GoFSH, SUSHI) without an embedded FHIR server.
Requires you to provide a URL for an external FHIR server when using features like the FHIR API Explorer and validation.
Does not require Git, Maven, or .NET for setup.
Ideal for users who will always connect to an existing external FHIR server.
Custom URL Mode
Similar to Lite Mode, but prompts you for a specific external FHIR server URL to use as the default for the toolkit.
The application will be pre-configured to use your custom URL for all FHIR-related operations.
Does not require Git, Maven, or .NET for setup.
Hapi Mode
Includes the full FHIRFLARE toolkit and an embedded HAPI FHIR server.
The docker-compose configuration will proxy requests to the internal HAPI server, which is accessible via http://localhost:8080/fhir.
Requires Git and Maven during the initial build process to compile the HAPI FHIR server.
Ideal for users who want a self-contained, offline development and testing environment.
Candle Mode
Includes the full FHIRFLARE toolkit and an embedded FHIR Candle server.
The docker-compose configuration will proxy requests to the internal Candle server, which is accessible via http://localhost:5001/fhir/<version>.
Requires Git and the .NET SDK during the initial build process.
Ideal for users who want to use the .NET-based FHIR server for development and testing.
* **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 - Hapi / Candle:**
* **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.
* **Standalone Version - Custom Mode:**
* Includes the full FHIRFLARE Toolkit application **and** point the Environmet variable at your chosen custom fhir url endpoint allowing for Custom setups
* **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.

200
app.py
View File

@ -48,9 +48,7 @@ from services import (
import_manual_package_and_dependencies,
parse_and_list_igs,
generate_ig_yaml,
apply_and_validate,
run_validator_cli,
uuid
apply_and_validate
)
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm, IgYamlForm
from wtforms import SubmitField
@ -72,16 +70,11 @@ app.config['FHIR_PACKAGES_DIR'] = os.path.join(instance_path, '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['LATEST_BUILD_URL'] = "http://build.fhir.org/ig/qas.json"
app.config['VALIDATION_LOG_DIR'] = os.path.join(app.instance_path, 'validation_logs')
os.makedirs(app.config['VALIDATION_LOG_DIR'], exist_ok=True)
app.config['UPLOAD_FOLDER'] = os.path.join(CURRENT_DIR, 'static', 'uploads') # For GoFSH output
app.config['APP_BASE_URL'] = os.environ.get('APP_BASE_URL', 'http://localhost:5000')
app.config['HAPI_FHIR_URL'] = os.environ.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')
CONFIG_PATH = os.environ.get('CONFIG_PATH', '/usr/local/tomcat/conf/application.yaml')
# Basic Swagger configuration
app.config['SWAGGER'] = {
'title': 'FHIRFLARE IG Toolkit API',
@ -543,29 +536,6 @@ def restart_tomcat():
def config_hapi():
return render_template('config_hapi.html', site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
@app.route('/api/get-local-server-url', methods=['GET'])
@swag_from({
'tags': ['FHIR Server Configuration'],
'summary': 'Get the local FHIR server URL.',
'description': 'Retrieves the base URL of the configured local FHIR server (HAPI). This is used by frontend components to make direct requests, bypassing the proxy.',
'responses': {
'200': {
'description': 'The URL of the local FHIR server.',
'schema': {
'type': 'object',
'properties': {
'url': {'type': 'string', 'example': 'http://localhost:8080/fhir'}
}
}
}
}
})
def get_local_server_url():
"""
Expose the local HAPI FHIR server URL to the frontend.
"""
return jsonify({'url': app.config.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')})
@app.route('/manual-import-ig', methods=['GET', 'POST'])
def manual_import_ig():
"""
@ -1772,157 +1742,6 @@ csrf.exempt(api_push_ig) # Add this line
# Exempt the entire API blueprint (for routes defined IN services.py, like /api/validate-sample)
csrf.exempt(services_bp) # Keep this line for routes defined in the blueprint
@app.route('/validate-sample', methods=['GET'])
def validate_resource_page():
"""Renders the validation form page."""
form = ValidationForm()
# Fetch list of packages to populate the dropdown
packages = current_app.config.get('MANUAL_PACKAGE_CACHE', [])
return render_template('validate_sample.html', form=form, packages=packages)
@app.route('/api/validate', methods=['POST'])
@swag_from({
'tags': ['Validation'],
'summary': 'Starts a validation process for a FHIR resource or bundle.',
'description': 'Starts a validation task and immediately returns a task ID for polling. The actual validation is run in a separate thread to prevent timeouts.',
'security': [{'ApiKeyAuth': []}],
'consumes': ['application/json'],
'parameters': [
{
'name': 'validation_payload',
'in': 'body',
'required': True,
'schema': {
'type': 'object',
'required': ['package_name', 'version', 'fhir_resource'],
'properties': {
'package_name': {'type': 'string', 'example': 'hl7.fhir.us.core'},
'version': {'type': 'string', 'example': '6.1.0'},
'fhir_resource': {'type': 'string', 'description': 'A JSON string of the FHIR resource or Bundle to validate.'},
'fhir_version': {'type': 'string', 'example': '4.0.1'},
'do_native': {'type': 'boolean', 'default': False},
'hint_about_must_support': {'type': 'boolean', 'default': False},
'assume_valid_rest_references': {'type': 'boolean', 'default': False},
'no_extensible_binding_warnings': {'type': 'boolean', 'default': False},
'show_times': {'type': 'boolean', 'default': False},
'allow_example_urls': {'type': 'boolean', 'default': False},
'check_ips_codes': {'type': 'boolean', 'default': False},
'allow_any_extensions': {'type': 'boolean', 'default': False},
'tx_routing': {'type': 'boolean', 'default': False},
'snomed_ct_version': {'type': 'string', 'default': ''},
'profiles': {'type': 'string', 'description': 'Comma-separated list of profile URLs.'},
'extensions': {'type': 'string', 'description': 'Comma-separated list of extension URLs.'},
'terminology_server': {'type': 'string', 'description': 'URL of an alternate terminology server.'}
}
}
}
],
'responses': {
'202': {'description': 'Validation process started successfully, returns a task_id.'},
'400': {'description': 'Invalid request (e.g., missing fields, invalid JSON).'}
}
})
def validate_resource():
"""API endpoint to run validation on a FHIR resource."""
try:
# Get data from the JSON payload
data = request.get_json()
if not data:
return jsonify({'status': 'error', 'message': 'Invalid JSON: No data received.'}), 400
fhir_resource_text = data.get('fhir_resource')
fhir_version = data.get('fhir_version')
package_name = data.get('package_name')
version = data.get('version')
profiles = data.get('profiles')
extensions = data.get('extensions')
terminology_server = data.get('terminology_server')
snomed_ct_version = data.get('snomed_ct_version')
# Build the list of IGs from the package name and version
igs = [f"{package_name}#{version}"] if package_name and version else []
flags = {
'doNative': data.get('do_native'),
'hintAboutMustSupport': data.get('hint_about_must_support'),
'assumeValidRestReferences': data.get('assume_valid_rest_references'),
'noExtensibleBindingWarnings': data.get('no_extensible_binding_warnings'),
'showTimes': data.get('show_times'),
'allowExampleUrls': data.get('allow_example_urls'),
'checkIpsCodes': data.get('check_ips_codes'),
'allowAnyExtensions': data.get('allow_any_extensions'),
'txRouting': data.get('tx_routing'),
}
# Check for empty resource
if not fhir_resource_text:
return jsonify({'status': 'error', 'message': 'No FHIR resource provided.'}), 400
try:
# Generate a unique task ID
task_id = str(uuid.uuid4())
# Store a pending status in the global cache
with services.cache_lock:
services.validation_results_cache[task_id] = {"status": "pending"}
# Start the validation process in a new thread
app_context = app.app_context()
thread = threading.Thread(
target=services.validate_in_thread,
args=(app_context, task_id, fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version),
daemon=True
)
thread.start()
logger.info(f"Validation task {task_id} started in background thread.")
return jsonify({"status": "accepted", "message": "Validation started", "task_id": task_id}), 202
except Exception as e:
logger.error(f"Error starting validation thread: {e}", exc_info=True)
return jsonify({'status': 'error', 'message': f'An internal server error occurred while starting the validation: {e}'}), 500
except Exception as e:
logger.error(f"Validation API error: {e}")
return jsonify({'status': 'error', 'message': f'An internal server error occurred: {e}'}), 500
@app.route('/api/check-validation-status/<task_id>', methods=['GET'])
@swag_from({
'tags': ['Validation'],
'summary': 'Check the status of a validation task.',
'description': 'Polls for the result of a validation task by its ID. Returns the full validation report when the task is complete.',
'security': [{'ApiKeyAuth': []}],
'parameters': [
{'name': 'task_id', 'in': 'path', 'type': 'string', 'required': True, 'description': 'The unique ID of the validation task.'}
],
'responses': {
'200': {'description': 'Returns the full validation report if the task is complete.'},
'202': {'description': 'The validation task is still in progress.'},
'404': {'description': 'The specified task ID was not found.'}
}
})
def check_validation_status(task_id):
"""
This GET endpoint is polled by the frontend to check the status of a validation task.
"""
with services.cache_lock:
task_result = services.validation_results_cache.get(task_id)
if task_result is None:
return jsonify({"status": "error", "message": "Task not found."}), 404
if task_result["status"] == "pending":
return jsonify({"status": "pending", "message": "Validation is still in progress."}), 202
# Remove the result from the cache after it has been retrieved to prevent a memory leak
with services.cache_lock:
del services.validation_results_cache[task_id]
return jsonify({"status": "complete", "result": task_result["result"]}), 200
def create_db():
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
try:
@ -3343,23 +3162,14 @@ def ig_configurator():
@app.route('/download/<path:filename>')
def download_file(filename):
"""
Serves a file from the temporary directory created during validation.
The filename includes the unique temporary directory name to prevent
unauthorized file access and to correctly locate the file.
Serves a file from the temporary directory.
This route is necessary to allow the user to download the validator reports.
"""
# Use the stored temporary directory path from app.config
temp_dir = current_app.config.get('VALIDATION_TEMP_DIR')
# If no temporary directory is set, or it's a security risk, return a 404.
if not temp_dir or not os.path.exists(temp_dir):
logger.error(f"Download request failed: Temporary directory not found or expired. Path: {temp_dir}")
return jsonify({"error": "File not found or validation session expired."}), 404
# Construct the full file path.
# The temporary directory path is hardcoded for security
temp_dir = tempfile.gettempdir()
file_path = os.path.join(temp_dir, filename)
if not os.path.exists(file_path):
logger.error(f"File not found for download: {file_path}")
return jsonify({"error": "File not found."}), 404
try:

View File

@ -3,13 +3,14 @@ services:
fhirflare:
build:
context: .
dockerfile: Dockerfile.lite
dockerfile: Dockerfile
ports:
- "5000:5000"
- "8080:8080"
- "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
@ -17,5 +18,5 @@ services:
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=standalone
- APP_BASE_URL=http://localhost:5000
- HAPI_FHIR_URL=https://smile.sparked-fhir.com/aucore/fhir/DEFAULT/
- HAPI_FHIR_URL=https://fhir.hl7.org.au/aucore/fhir/DEFAULT/
command: supervisord -c /etc/supervisord.conf

View File

@ -311,59 +311,4 @@ class FhirRequestForm(FlaskForm):
class IgYamlForm(FlaskForm):
"""Form to select IGs for YAML generation."""
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
generate_yaml = SubmitField('Generate YAML')
# --- New ValidationForm class for the new page ---
class ValidationForm(FlaskForm):
"""Form for validating a single FHIR resource with various options."""
# Added fields to match the HTML template
package_name = StringField('Package Name', validators=[Optional()])
version = StringField('Package Version', validators=[Optional()])
# Main content fields
fhir_resource = TextAreaField('FHIR Resource (JSON or XML)', validators=[InputRequired()],
render_kw={'placeholder': 'Paste your FHIR JSON here...'})
fhir_version = SelectField('FHIR Version', choices=[
('4.0.1', 'R4 (4.0.1)'),
('3.0.2', 'STU3 (3.0.2)'),
('5.0.0', 'R5 (5.0.0)')
], default='4.0.1', validators=[DataRequired()])
# Flags and options from validator settings.pdf
do_native = BooleanField('Native Validation (doNative)')
hint_about_must_support = BooleanField('Must Support (hintAboutMustSupport)')
assume_valid_rest_references = BooleanField('Assume Valid Rest References (assumeValidRestReferences)')
no_extensible_binding_warnings = BooleanField('Extensible Binding Warnings (noExtensibleBindingWarnings)')
show_times = BooleanField('Show Times (-show-times)')
allow_example_urls = BooleanField('Allow Example URLs (-allow-example-urls)')
check_ips_codes = BooleanField('Check IPS Codes (-check-ips-codes)')
allow_any_extensions = BooleanField('Allow Any Extensions (-allow-any-extensions)')
tx_routing = BooleanField('Show Terminology Routing (-tx-routing)')
# SNOMED CT options
snomed_ct_version = SelectField('Select SNOMED Version', choices=[
('', '-- No selection --'),
('intl', 'International edition (900000000000207008)'),
('us', 'US edition (731000124108)'),
('uk', 'United Kingdom Edition (999000041000000102)'),
('es', 'Spanish Language Edition (449081005)'),
('nl', 'Netherlands Edition (11000146104)'),
('ca', 'Canadian Edition (20611000087101)'),
('dk', 'Danish Edition (554471000005108)'),
('se', 'Swedish Edition (45991000052106)'),
('au', 'Australian Edition (32506021000036107)'),
('be', 'Belgium Edition (11000172109)')
], validators=[Optional()])
# Text input fields
profiles = StringField('Profiles',
validators=[Optional()],
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'})
extensions = StringField('Extensions',
validators=[Optional()],
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace'})
terminology_server = StringField('Terminology Server',
validators=[Optional()],
render_kw={'placeholder': 'e.g. http://tx.fhir.org'})
submit = SubmitField('Validate')
generate_yaml = SubmitField('Generate YAML')

View File

@ -198,203 +198,6 @@ def safe_parse_version(v_str):
logger.error(f"Unexpected error in safe_parse_version for '{v_str}': {e}")
return pkg_version.parse("0.0.0a0") # Fallback
# --- New Function to run the FHIR Validator CLI ---
def run_validator_cli(resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version, app_config):
"""
Constructs and runs the Java FHIR Validator CLI command.
Args:
resource_text (str): The FHIR resource/bundle as a string.
fhir_version (str): The FHIR version to validate against.
igs (list): List of selected IG canonical URLs.
profiles (str): Comma-separated list of profile URLs.
extensions (str): Comma-separated list of extension URLs.
terminology_server (str): The URL of the terminology server.
flags (dict): Dictionary of boolean flags for validation.
snomed_ct_version (str): The SNOMED CT version to use.
app_config (dict): The Flask application configuration.
Returns:
dict: A dictionary containing the validation output and status.
"""
temp_dir = tempfile.mkdtemp(dir=app_config['VALIDATION_LOG_DIR'])
# Store the temp directory in the app config for the download endpoint to find it.
app_config['VALIDATION_TEMP_DIR'] = temp_dir
temp_resource_path = os.path.join(temp_dir, f'resource_{uuid.uuid4()}.json')
try:
with open(temp_resource_path, 'w', encoding='utf-8') as f:
f.write(resource_text)
except Exception as e:
shutil.rmtree(temp_dir)
return {'status': 'error', 'message': f'Failed to write temporary resource file: {e}'}
output_json_path = os.path.join(temp_dir, 'validation-report.json')
output_html_path = os.path.join(temp_dir, 'validation-report.html')
command = [
'java',
'-jar', os.environ.get('JAVA_VALIDATOR_PATH', '/app/validator_cli/validator_cli.jar'),
temp_resource_path,
'-version', fhir_version,
'-output', output_json_path,
'-html-output', output_html_path,
]
# Add flags
if flags.get('doNative'):
command.append('-do-native')
if flags.get('hintAboutMustSupport'):
command.append('-hint-about-must-support')
if flags.get('assumeValidRestReferences'):
command.append('-assume-valid-rest-references')
if flags.get('noExtensibleBindingWarnings'):
command.append('-no-extensible-binding-warnings')
if flags.get('showTimes'):
command.append('-show-times')
if flags.get('allowExampleUrls'):
command.append('-allow-example-urls')
if flags.get('checkIpsCodes'):
command.append('-check-ips-codes')
if flags.get('allowAnyExtensions'):
command.append('-allow-any-extensions')
if flags.get('txRouting'):
command.append('-tx-routing')
# Add other options
if igs:
for ig in igs:
command.extend(['-ig', ig])
if profiles:
command.extend(['-profile', profiles])
if extensions:
command.extend(['-extension', extensions])
if terminology_server:
command.extend(['-tx', terminology_server])
if snomed_ct_version:
command.extend(['-sct', snomed_ct_version])
logger.info(f"Running validator with command: {' '.join(command)}")
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
check=False,
timeout=300,
)
stdout = result.stdout
stderr = result.stderr
logger.debug(f"Validator stdout:\n{stdout}")
logger.debug(f"Validator stderr:\n{stderr}")
# Check for errors in the stdout and stderr
if result.returncode != 0 and not os.path.exists(output_json_path):
return_dict = {
'status': 'error',
'output': stderr or "Validation process failed with an unknown error.",
'file_paths': {},
'errors': [stderr or "Validation process failed with an unknown error."],
'warnings': [],
'summary': {'error_count': 1, 'warning_count': 0},
'results': {}
}
return return_dict
# Now, try to read the JSON file produced by the validator
if os.path.exists(output_json_path):
with open(output_json_path, 'r', encoding='utf-8') as f:
json_report = json.load(f)
# Process the JSON report to get a structured summary
error_count = 0
warning_count = 0
errors_list = []
warnings_list = []
detailed_results = {}
is_valid = True
for issue in json_report.get('issue', []):
severity = issue.get('severity')
diagnostics = issue.get('diagnostics') or issue.get('details', {}).get('text') or 'No details provided.'
resource_expression = issue.get('expression', ['unknown'])[0]
# Determine the resource ID and type from the expression, e.g., "AllergyIntolerance" or "AllergyIntolerance[0].extension[0]"
resource_id = resource_expression.split('[')[0].split('.')[0]
if resource_id not in detailed_results:
detailed_results[resource_id] = {'valid': True, 'details': []}
detail = {
'issue': diagnostics,
'severity': severity,
'description': issue.get('details', {}).get('text', diagnostics)
}
detailed_results[resource_id]['details'].append(detail)
if severity == 'error':
error_count += 1
errors_list.append(diagnostics)
is_valid = False
detailed_results[resource_id]['valid'] = False
elif severity == 'warning':
warning_count += 1
warnings_list.append(diagnostics)
# Determine the overall status
status = 'success'
if error_count > 0:
status = 'error'
elif warning_count > 0:
status = 'warning'
# Build the final return dictionary
return_dict = {
'status': status,
'valid': is_valid,
'output': stdout,
'file_paths': {
'json': output_json_path,
'html': output_html_path
},
'errors': errors_list,
'warnings': warnings_list,
'summary': {
'error_count': error_count,
'warning_count': warning_count
},
'results': detailed_results
}
return return_dict
else:
# Fallback if validator completed but didn't produce a file
return_dict = {
'status': 'error',
'valid': False,
'output': stdout,
'file_paths': {},
'errors': ["Validator failed to produce the expected output files."],
'warnings': [],
'summary': {'error_count': 1, 'warning_count': 0},
'results': {}
}
return return_dict
except FileNotFoundError:
return {'status': 'error', 'message': 'Java command or validator JAR not found. Please check paths.'}
except subprocess.TimeoutExpired:
return {'status': 'error', 'message': 'Validation timed out.'}
except Exception as e:
return {'status': 'error', 'message': f'An unexpected error occurred during validation: {e}'}
finally:
# NOTE: We no longer delete the temporary directory here.
# It is the responsibility of the `clear-validation-results` endpoint.
pass
# --- MODIFIED FUNCTION with Enhanced Logging ---
def get_additional_registries():
"""Fetches the list of additional FHIR IG registries from the master feed."""
@ -1305,16 +1108,15 @@ validation_results_cache = {}
# Thread lock to safely manage access to the cache
cache_lock = threading.Lock()
def validate_in_thread(app_context, task_id, fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version):
def validate_in_thread(app_context, task_id, package_name, version, sample_data):
"""
Wrapper function to run the blocking validator in a separate thread.
It acquires an app context and then calls the main validation logic.
"""
with app_context:
logger.info(f"Starting validation thread for task ID: {task_id}")
# Call the core blocking validation function, passing all arguments
result = run_validator_cli(fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version, current_app.config)
# Call the core blocking validation function
result = run_java_validator(package_name, version, sample_data)
# Safely update the shared cache with the final result
with cache_lock:

View File

@ -1,21 +1,18 @@
#!/bin/bash
# --- Configuration ---
REPO_URL_HAPI="https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git"
REPO_URL_CANDLE="https://github.com/FHIR/fhir-candle.git"
CLONE_DIR_HAPI="hapi-fhir-jpaserver"
CLONE_DIR_CANDLE="fhir-candle"
SOURCE_CONFIG_DIR="hapi-fhir-Setup"
REPO_URL="https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git"
CLONE_DIR="hapi-fhir-jpaserver"
SOURCE_CONFIG_DIR="hapi-fhir-Setup" # Assuming this is relative to the script's parent
CONFIG_FILE="application.yaml"
# --- Define Paths ---
# Note: Adjust SOURCE_CONFIG_PATH if SOURCE_CONFIG_DIR is not a sibling directory
# This assumes the script is run from a directory, and hapi-fhir-setup is at the same level
SOURCE_CONFIG_PATH="../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
DEST_CONFIG_PATH="${CLONE_DIR_HAPI}/target/classes/${CONFIG_FILE}"
DEST_CONFIG_PATH="${CLONE_DIR}/target/classes/${CONFIG_FILE}"
APP_MODE=""
CUSTOM_FHIR_URL_VAL=""
SERVER_TYPE=""
CANDLE_FHIR_VERSION=""
# --- Error Handling Function ---
handle_error() {
@ -23,39 +20,25 @@ handle_error() {
echo "An error occurred: $1"
echo "Script aborted."
echo "------------------------------------"
# Removed 'read -p "Press Enter to exit..."' as it's not typical for non-interactive CI/CD
exit 1
}
# === MODIFIED: Prompt for Installation Mode ===
# === Prompt for Installation Mode ===
get_mode_choice() {
echo ""
echo "Select Installation Mode:"
echo "1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet needed)"
echo "2. Custom URL (Uses a custom FHIR Server - No Git/Maven/Dotnet needed)"
echo "3. Hapi (Includes local HAPI FHIR Server - Requires Git & Maven)"
echo "4. Candle (Includes local FHIR Candle Server - Requires Git & Dotnet)"
echo "1. Standalone (Includes local HAPI FHIR Server - Requires Git & Maven)"
echo "2. Lite (Excludes local HAPI FHIR Server - No Git/Maven needed)"
while true; do
read -r -p "Enter your choice (1, 2, 3, or 4): " choice
read -r -p "Enter your choice (1 or 2): " choice
case "$choice" in
1)
APP_MODE="lite"
APP_MODE="standalone"
break
;;
2)
APP_MODE="standalone"
get_custom_url_prompt
break
;;
3)
APP_MODE="standalone"
SERVER_TYPE="hapi"
break
;;
4)
APP_MODE="standalone"
SERVER_TYPE="candle"
get_candle_fhir_version
APP_MODE="lite"
break
;;
*)
@ -64,301 +47,156 @@ get_mode_choice() {
esac
done
echo "Selected Mode: $APP_MODE"
echo "Server Type: $SERVER_TYPE"
echo
}
# === NEW: Prompt for Custom URL ===
get_custom_url_prompt() {
local confirmed_url=""
while true; do
echo
read -r -p "Please enter the custom FHIR server URL: " custom_url_input
echo
echo "You entered: $custom_url_input"
read -r -p "Is this URL correct? (Y/N): " confirm_url
if [[ "$confirm_url" =~ ^[Yy]$ ]]; then
confirmed_url="$custom_url_input"
break
else
echo "URL not confirmed. Please re-enter."
fi
done
while true; do
echo
read -r -p "Please re-enter the URL to confirm it is correct: " custom_url_input
if [ "$custom_url_input" = "$confirmed_url" ]; then
CUSTOM_FHIR_URL_VAL="$custom_url_input"
echo
echo "Custom URL confirmed: $CUSTOM_FHIR_URL_VAL"
break
else
echo
echo "URLs do not match. Please try again."
confirmed_url="$custom_url_input"
fi
done
}
# === NEW: Prompt for Candle FHIR version ===
get_candle_fhir_version() {
echo ""
echo "Select the FHIR version for the Candle server:"
echo "1. R4 (4.0)"
echo "2. R4B (4.3)"
echo "3. R5 (5.0)"
while true; do
read -r -p "Enter your choice (1, 2, or 3): " choice
case "$choice" in
1)
CANDLE_FHIR_VERSION=r4
break
;;
2)
CANDLE_FHIR_VERSION=r4b
break
;;
3)
CANDLE_FHIR_VERSION=r5
break
;;
*)
echo "Invalid input. Please try again."
;;
esac
done
}
# Call the function to get mode choice
get_mode_choice
# === Conditionally Execute Server Setup ===
case "$SERVER_TYPE" in
"hapi")
echo "Running Hapi server setup..."
echo
# === Conditionally Execute HAPI Setup ===
if [ "$APP_MODE" = "standalone" ]; then
echo "Running Standalone setup including HAPI FHIR..."
echo
# --- Step 0: Clean up previous clone (optional) ---
echo "Checking for existing directory: $CLONE_DIR_HAPI"
if [ -d "$CLONE_DIR_HAPI" ]; then
echo "Found existing directory, removing it..."
rm -rf "$CLONE_DIR_HAPI"
if [ $? -ne 0 ]; then
handle_error "Failed to remove existing directory: $CLONE_DIR_HAPI"
fi
echo "Existing directory removed."
else
echo "Directory does not exist, proceeding with clone."
fi
echo
# --- Step 1: Clone the HAPI FHIR server repository ---
echo "Cloning repository: $REPO_URL_HAPI into $CLONE_DIR_HAPI..."
git clone "$REPO_URL_HAPI" "$CLONE_DIR_HAPI"
# --- Step 0: Clean up previous clone (optional) ---
echo "Checking for existing directory: $CLONE_DIR"
if [ -d "$CLONE_DIR" ]; then
echo "Found existing directory, removing it..."
rm -rf "$CLONE_DIR"
if [ $? -ne 0 ]; then
handle_error "Failed to clone repository. Check Git installation and network connection."
handle_error "Failed to remove existing directory: $CLONE_DIR"
fi
echo "Repository cloned successfully."
echo
echo "Existing directory removed."
else
echo "Directory does not exist, proceeding with clone."
fi
echo
# --- Step 2: Navigate into the cloned directory ---
echo "Changing directory to $CLONE_DIR_HAPI..."
cd "$CLONE_DIR_HAPI" || handle_error "Failed to change directory to $CLONE_DIR_HAPI."
echo "Current directory: $(pwd)"
echo
# --- Step 1: Clone the HAPI FHIR server repository ---
echo "Cloning repository: $REPO_URL into $CLONE_DIR..."
git clone "$REPO_URL" "$CLONE_DIR"
if [ $? -ne 0 ]; then
handle_error "Failed to clone repository. Check Git installation and network connection."
fi
echo "Repository cloned successfully."
echo
# --- Step 3: Build the HAPI server using Maven ---
echo "===> Starting Maven build (Step 3)..."
mvn clean package -DskipTests=true -Pboot
# --- Step 2: Navigate into the cloned directory ---
echo "Changing directory to $CLONE_DIR..."
cd "$CLONE_DIR" || handle_error "Failed to change directory to $CLONE_DIR."
echo "Current directory: $(pwd)"
echo
# --- Step 3: Build the HAPI server using Maven ---
echo "===> Starting Maven build (Step 3)..."
mvn clean package -DskipTests=true -Pboot
if [ $? -ne 0 ]; then
echo "ERROR: Maven build failed."
cd ..
handle_error "Maven build process resulted in an error."
fi
echo "Maven build completed successfully."
echo
# --- Step 4: Copy the configuration file ---
echo "===> Starting file copy (Step 4)..."
echo "Copying configuration file..."
# Corrected SOURCE_CONFIG_PATH to be relative to the new current directory ($CLONE_DIR)
# This assumes the original script's SOURCE_CONFIG_PATH was relative to its execution location
# If SOURCE_CONFIG_DIR is ../hapi-fhir-setup relative to script's original location:
# Then from within CLONE_DIR, it becomes ../../hapi-fhir-setup
# We defined SOURCE_CONFIG_PATH earlier relative to the script start.
# So, when inside CLONE_DIR, the path from original script location should be used.
# The original script had: set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
# And then: xcopy "%SOURCE_CONFIG_PATH%" "target\classes\"
# This implies SOURCE_CONFIG_PATH is relative to the original script's location, not the $CLONE_DIR
# Therefore, we need to construct the correct relative path from *within* $CLONE_DIR back to the source.
# Assuming the script is in dir X, and SOURCE_CONFIG_DIR is ../hapi-fhir-setup from X.
# So, hapi-fhir-setup is a sibling of X's parent.
# If CLONE_DIR is also in X, then from within CLONE_DIR, the path is ../ + original SOURCE_CONFIG_PATH
# For simplicity and robustness, let's use an absolute path or a more clearly defined relative path from the start.
# The original `SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%` implies
# that `hapi-fhir-setup` is a sibling of the directory where the script *is being run from*.
# Let's assume the script is run from the root of FHIRFLARE-IG-Toolkit.
# And hapi-fhir-setup is also in the root, next to this script.
# Then SOURCE_CONFIG_PATH would be ./hapi-fhir-setup/target/classes/application.yaml
# And from within ./hapi-fhir-jpaserver/, the path would be ../hapi-fhir-setup/target/classes/application.yaml
# The original batch file sets SOURCE_CONFIG_PATH as "..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%"
# And COPIES it to "target\classes\" *while inside CLONE_DIR*.
# This means the source path is relative to where the *cd %CLONE_DIR%* happened from.
# Let's make it relative to the script's initial execution directory.
INITIAL_SCRIPT_DIR=$(pwd)
ABSOLUTE_SOURCE_CONFIG_PATH="${INITIAL_SCRIPT_DIR}/../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}" # This matches the ..\ logic
echo "Source: $ABSOLUTE_SOURCE_CONFIG_PATH"
echo "Destination: target/classes/$CONFIG_FILE"
if [ ! -f "$ABSOLUTE_SOURCE_CONFIG_PATH" ]; then
echo "WARNING: Source configuration file not found at $ABSOLUTE_SOURCE_CONFIG_PATH."
echo "The script will continue, but the server might use default configuration."
else
cp "$ABSOLUTE_SOURCE_CONFIG_PATH" "target/classes/"
if [ $? -ne 0 ]; then
echo "ERROR: Maven build failed."
cd ..
handle_error "Maven build process resulted in an error."
fi
echo "Maven build completed successfully."
echo
# --- Step 4: Copy the configuration file ---
echo "===> Starting file copy (Step 4)..."
echo "Copying configuration file..."
INITIAL_SCRIPT_DIR=$(pwd)
ABSOLUTE_SOURCE_CONFIG_PATH="${INITIAL_SCRIPT_DIR}/../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
echo "Source: $ABSOLUTE_SOURCE_CONFIG_PATH"
echo "Destination: target/classes/$CONFIG_FILE"
if [ ! -f "$ABSOLUTE_SOURCE_CONFIG_PATH" ]; then
echo "WARNING: Source configuration file not found at $ABSOLUTE_SOURCE_CONFIG_PATH."
echo "WARNING: Failed to copy configuration file. Check if the source file exists and permissions."
echo "The script will continue, but the server might use default configuration."
else
cp "$ABSOLUTE_SOURCE_CONFIG_PATH" "target/classes/"
if [ $? -ne 0 ]; then
echo "WARNING: Failed to copy configuration file. Check if the source file exists and permissions."
echo "The script will continue, but the server might use default configuration."
else
echo "Configuration file copied successfully."
fi
echo "Configuration file copied successfully."
fi
echo
fi
echo
# --- Step 5: Navigate back to the parent directory ---
echo "===> Changing directory back (Step 5)..."
cd .. || handle_error "Failed to change back to the parent directory."
echo "Current directory: $(pwd)"
echo
;;
# --- Step 5: Navigate back to the parent directory ---
echo "===> Changing directory back (Step 5)..."
cd .. || handle_error "Failed to change back to the parent directory."
echo "Current directory: $(pwd)"
echo
"candle")
echo "Running FHIR Candle server setup..."
echo
# --- Step 0: Clean up previous clone (optional) ---
echo "Checking for existing directory: $CLONE_DIR_CANDLE"
if [ -d "$CLONE_DIR_CANDLE" ]; then
echo "Found existing directory, removing it..."
rm -rf "$CLONE_DIR_CANDLE"
if [ $? -ne 0 ]; then
handle_error "Failed to remove existing directory: $CLONE_DIR_CANDLE"
fi
echo "Existing directory removed."
else
echo "Directory does not exist, proceeding with clone."
fi
echo
else # APP_MODE is "lite"
echo "Running Lite setup, skipping HAPI FHIR build..."
# Ensure the hapi-fhir-jpaserver directory doesn't exist or is empty if Lite mode is chosen
if [ -d "$CLONE_DIR" ]; then
echo "Found existing HAPI directory ($CLONE_DIR) in Lite mode. Removing it..."
rm -rf "$CLONE_DIR"
fi
# Create empty target directories expected by Dockerfile COPY, even if not used
mkdir -p "${CLONE_DIR}/target/classes"
mkdir -p "${CLONE_DIR}/custom" # This was in the original batch, ensure it's here
# Create a placeholder empty WAR file and application.yaml to satisfy Dockerfile COPY
touch "${CLONE_DIR}/target/ROOT.war"
touch "${CLONE_DIR}/target/classes/application.yaml"
echo "Placeholder files and directories created for Lite mode build in $CLONE_DIR."
echo
fi
# --- Step 1: Clone the FHIR Candle server repository ---
echo "Cloning repository: $REPO_URL_CANDLE into $CLONE_DIR_CANDLE..."
git clone "$REPO_URL_CANDLE" "$CLONE_DIR_CANDLE"
if [ $? -ne 0 ]; then
handle_error "Failed to clone repository. Check Git and Dotnet SDK installation and network connection."
fi
echo "Repository cloned successfully."
echo
# --- Step 2: Navigate into the cloned directory ---
echo "Changing directory to $CLONE_DIR_CANDLE..."
cd "$CLONE_DIR_CANDLE" || handle_error "Failed to change directory to $CLONE_DIR_CANDLE."
echo "Current directory: $(pwd)"
echo
# --- Step 3: Build the FHIR Candle server using Dotnet ---
echo "===> Starting Dotnet build (Step 3)..."
dotnet publish -c Release -f net9.0 -o publish
if [ $? -ne 0 ]; then
handle_error "Dotnet build failed. Check Dotnet SDK installation."
fi
echo "Dotnet build completed successfully."
echo
# --- Step 4: Navigate back to the parent directory ---
echo "===> Changing directory back (Step 4)..."
cd .. || handle_error "Failed to change back to the parent directory."
echo "Current directory: $(pwd)"
echo
;;
*) # APP_MODE is Lite, no SERVER_TYPE
echo "Running Lite setup, skipping server build..."
if [ -d "$CLONE_DIR_HAPI" ]; then
echo "Found existing HAPI directory in Lite mode. Removing it to avoid build issues..."
rm -rf "$CLONE_DIR_HAPI"
fi
if [ -d "$CLONE_DIR_CANDLE" ]; then
echo "Found existing Candle directory in Lite mode. Removing it to avoid build issues..."
rm -rf "$CLONE_DIR_CANDLE"
fi
mkdir -p "${CLONE_DIR_HAPI}/target/classes"
mkdir -p "${CLONE_DIR_HAPI}/custom"
touch "${CLONE_DIR_HAPI}/target/ROOT.war"
touch "${CLONE_DIR_HAPI}/target/classes/application.yaml"
mkdir -p "${CLONE_DIR_CANDLE}/publish"
touch "${CLONE_DIR_CANDLE}/publish/fhir-candle.dll"
echo "Placeholder files and directories created for Lite mode build."
echo
;;
esac
# === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL and DOCKERFILE ===
echo "Updating docker-compose.yml with APP_MODE=$APP_MODE and HAPI_FHIR_URL..."
# === Modify docker-compose.yml to set APP_MODE ===
echo "Updating docker-compose.yml with APP_MODE=$APP_MODE..."
DOCKER_COMPOSE_TMP="docker-compose.yml.tmp"
DOCKER_COMPOSE_ORIG="docker-compose.yml"
HAPI_URL_TO_USE="https://fhir.hl7.org.au/aucore/fhir/DEFAULT/"
if [ -n "$CUSTOM_FHIR_URL_VAL" ]; then
HAPI_URL_TO_USE="$CUSTOM_FHIR_URL_VAL"
elif [ "$SERVER_TYPE" = "candle" ]; then
HAPI_URL_TO_USE="http://localhost:5826/fhir/${CANDLE_FHIR_VERSION}"
else
HAPI_URL_TO_USE="http://localhost:8080/fhir"
fi
DOCKERFILE_TO_USE="Dockerfile.lite"
if [ "$SERVER_TYPE" = "hapi" ]; then
DOCKERFILE_TO_USE="Dockerfile.hapi"
elif [ "$SERVER_TYPE" = "candle" ]; then
DOCKERFILE_TO_USE="Dockerfile.candle"
fi
cat << EOF > "$DOCKER_COMPOSE_TMP"
version: '3.8'
services:
fhirflare:
build:
context: .
dockerfile: ${DOCKERFILE_TO_USE}
dockerfile: Dockerfile
ports:
EOF
if [ "$SERVER_TYPE" = "candle" ]; then
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- "5000:5000"
- "5001:5826"
EOF
else
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- "5000:5000"
- "8080:8080"
EOF
fi
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
volumes:
- ./instance:/app/instance
- ./static/uploads:/app/static/uploads
- ./logs:/app/logs
EOF
if [ "$SERVER_TYPE" = "hapi" ]; then
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
- ./hapi-fhir-jpaserver/target/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
- ./hapi-fhir-jpaserver/target/classes/application.yaml:/usr/local/tomcat/conf/application.yaml
EOF
elif [ "$SERVER_TYPE" = "candle" ]; then
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- ./fhir-candle/publish/:/app/fhir-candle-publish/
EOF
fi
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- ./logs:/app/logs
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=${APP_MODE}
- APP_BASE_URL=http://localhost:5000
- HAPI_FHIR_URL=${HAPI_URL_TO_USE}
EOF
if [ "$SERVER_TYPE" = "candle" ]; then
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- ASPNETCORE_URLS=http://0.0.0.0:5826
EOF
fi
cat << EOF >> "$DOCKER_COMPOSE_TMP"
- HAPI_FHIR_URL=http://localhost:8080/fhir
command: supervisord -c /etc/supervisord.conf
EOF

View File

@ -33,18 +33,4 @@ stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
stderr_logfile=/app/logs/tomcat_err.log
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=5
[program:candle]
command=dotnet /app/fhir-candle-publish/fhir-candle.dll
directory=/app/fhir-candle-publish
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=10
stdout_logfile=/app/logs/candle.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
stderr_logfile=/app/logs/candle_err.log
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=5
stderr_logfile_backups=5

View File

@ -40,7 +40,7 @@
<div class="row g-4">
<!-- LEFT: Downloaded Packages -->
<div class="col-md-5">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
<div class="card-body">
@ -65,20 +65,20 @@
</td>
<td>{{ pkg.version }}</td>
<td>
<div class="btn-group-vertical btn-group-sm w-80">
<div class="btn-group btn-group-sm">
{% if is_processed %}
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
{% else %}
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
{{ form.csrf_token }}
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-primary w-80"><i class="bi bi-gear"></i> Process</button>
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
{{ form.csrf_token }}
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-danger w-80" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
</form>
</div>
</td>
@ -105,7 +105,7 @@
</div>
<!-- RIGHT: Processed Packages -->
<div class="col-md-7">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
<div class="card-body">
@ -139,12 +139,12 @@
{% endif %}
</td>
<td>
<div class="btn-group-vertical btn-group-sm w-80">
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-80" title="View Details"><i class="bi bi-search"></i> View</a>
<div class="btn-group-vertical btn-group-sm w-100">
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
{{ form.csrf_token }}
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
<button type="submit" class="btn btn-outline-warning w-80 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
</form>
</div>
</td>

View File

@ -6,7 +6,7 @@
<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.
Interact with FHIR servers using GET, POST, PUT, or DELETE requests. Toggle between local Fhir Server or a custom server to explore resources or perform searches.
</p>
</div>
</div>
@ -21,11 +21,11 @@
<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 Server</span>
<span id="toggleLabel">Use Local </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>
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local (http://localhost:8080/fhir) or enter a custom FHIR server URL.</small>
</div>
<div class="mb-3" id="authSection" style="display: none;">
<label class="form-label">Authentication</label>
@ -126,7 +126,7 @@ document.addEventListener('DOMContentLoaded', function() {
const authSection = document.getElementById('authSection');
const authTypeSelect = document.getElementById('authType');
const authInputsGroup = document.getElementById('authInputsGroup');
const bearerTokenInput = document.getElementById('bearerTokenInput');
const bearerTokenInput = document.getElementById('bearerToken');
const basicAuthInputs = document.getElementById('basicAuthInputs');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
@ -212,7 +212,7 @@ document.addEventListener('DOMContentLoaded', function() {
toggleServerButton.style.pointerEvents = 'auto';
toggleServerButton.removeAttribute('aria-disabled');
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
toggleLabel.textContent = useLocalHapi ? 'Using Local Server' : 'Using Custom URL';
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
fhirServerUrlInput.required = !useLocalHapi;
@ -244,8 +244,7 @@ document.addEventListener('DOMContentLoaded', function() {
fhirServerUrlInput.value = '';
}
updateServerToggleUI();
updateAuthInputsUI();
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local Server' : 'Custom URL'}`);
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
}
function updateRequestBodyVisibility() {
@ -296,7 +295,7 @@ document.addEventListener('DOMContentLoaded', function() {
const customUrl = fhirServerUrlInput.value.trim();
let body = undefined;
const authType = authTypeSelect ? authTypeSelect.value : 'none';
const bearerToken = document.getElementById('bearerToken').value.trim();
const bearerToken = bearerTokenInput ? bearerTokenInput.value.trim() : '';
const username = usernameInput ? usernameInput.value.trim() : '';
const password = passwordInput ? passwordInput.value : '';
@ -335,7 +334,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (!useLocalHapi) {
if (authType === 'bearer' && !bearerToken) {
alert('Please enter a Bearer Token.');
document.getElementById('bearerToken').classList.add('is-invalid');
bearerTokenInput.classList.add('is-invalid');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
@ -454,7 +453,7 @@ document.addEventListener('DOMContentLoaded', function() {
} catch (error) {
console.error('Fetch error:', error);
responseCard.style.display = 'block';
responseStatus.textContent = 'Network Error';
responseStatus.textContent = `Network Error`;
responseStatus.className = 'badge bg-danger';
responseHeaders.textContent = 'N/A';
let errorDetail = `Error: ${error.message}\n\n`;

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
</p>
</div>
</div>
<div class="container mt-4">
<div class="card shadow-sm mb-4">
<div class="card-header">
@ -27,17 +28,18 @@
</div>
{% endif %}
<form id="retrieveForm" method="POST">
{{ form.csrf_token(id='retrieve_csrf_token') }}
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label fw-bold">FHIR Server</label>
<div class="input-group">
<button type="button" class="btn btn-outline-primary" id="toggleServer">
<span id="toggleLabel">Use Local Server</span>
<span id="toggleLabel">Use Local </span>
</button>
{{ form.fhir_server_url(class="form-control", id="fhirServerUrl", style="display: none;", placeholder="e.g., https://fhir.hl7.org.au/aucore/fhir/DEFAULT", **{'aria-describedby': 'fhirServerHelp'}) }}
</div>
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local Fhir Server(/fhir proxy) or enter a custom FHIR server URL.</small>
</div>
{# Authentication Section (Shown for Custom URL) #}
<div class="mb-3" id="authSection" style="display: none;">
<label class="form-label fw-bold">Authentication</label>
@ -66,6 +68,7 @@
</div>
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
</div>
{# Checkbox Row #}
<div class="row g-3 mb-3 align-items-center">
<div class="col-md-6">
@ -75,6 +78,7 @@
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
</div>
</div>
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
<div class="banner3 mt-3" id="resourceTypes" style="display: none;">
<h5>Resource Types</h5>
@ -98,13 +102,14 @@
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">
<h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4>
</div>
<div class="card-body">
<form id="splitForm" method="POST" enctype="multipart/form-data">
{{ form.csrf_token(id='split_csrf_token') }}
{{ form.hidden_tag() }}
<div class="mb-3">
<label class="form-label fw-bold">Bundle Source</label>
<div class="form-check">
@ -136,6 +141,7 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element References ---
@ -162,10 +168,11 @@ document.addEventListener('DOMContentLoaded', () => {
const authSection = document.getElementById('authSection');
const authTypeSelect = document.getElementById('authType');
const authInputsGroup = document.getElementById('authInputsGroup');
const bearerTokenInput = document.getElementById('bearerTokenInput');
const bearerTokenInput = document.getElementById('bearerToken');
const basicAuthInputs = document.getElementById('basicAuthInputs');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
// --- State & Config Variables ---
const appMode = '{{ app_mode | default("standalone") | lower }}';
const apiKey = '{{ api_key }}';
@ -174,9 +181,10 @@ document.addEventListener('DOMContentLoaded', () => {
let retrieveZipPath = null;
let splitZipPath = null;
let fetchedMetadataCache = null;
let localHapiBaseUrl = ''; // To store the fetched URL
// --- Helper Functions ---
const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
// --- UI Update Functions ---
function updateBundleSourceUI() {
const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
@ -187,23 +195,10 @@ document.addEventListener('DOMContentLoaded', () => {
splitBundleZipInput.required = selectedSource === 'upload';
}
}
async function fetchLocalUrlAndSetUI() {
try {
const response = await fetch('/api/get-local-server-url');
if (!response.ok) throw new Error('Failed to fetch local server URL.');
const data = await response.json();
localHapiBaseUrl = data.url.replace(/\/+$/, ''); // Normalize by removing trailing slashes
console.log(`Local HAPI URL fetched: ${localHapiBaseUrl}`);
} catch (error) {
console.error(error);
localHapiBaseUrl = '/fhir'; // Fallback to proxy
alert('Could not fetch local HAPI server URL. Falling back to proxy.');
} finally {
updateServerToggleUI();
}
}
function updateServerToggleUI() {
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return;
if (appMode === 'lite') {
useLocalHapi = false;
toggleServerButton.disabled = true;
@ -221,7 +216,7 @@ document.addEventListener('DOMContentLoaded', () => {
toggleServerButton.classList.remove('disabled');
toggleServerButton.style.pointerEvents = 'auto';
toggleServerButton.removeAttribute('aria-disabled');
toggleLabel.textContent = useLocalHapi ? 'Use Local Server' : 'Use Custom URL';
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
fhirServerUrlInput.required = !useLocalHapi;
@ -231,22 +226,18 @@ document.addEventListener('DOMContentLoaded', () => {
updateAuthInputsUI();
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
}
function updateAuthInputsUI() {
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
const authType = authTypeSelect.value;
authInputsGroup.style.display = (authType === 'bearer' || authType === 'basic') ? 'block' : 'none';
bearerTokenInput.style.display = authType === 'bearer' ? 'block' : 'none';
basicAuthInputs.style.display = authType === 'basic' ? 'block' : 'none';
if (authType !== 'bearer') {
// Correctly reference the input element by its ID
const tokenInput = document.getElementById('bearerToken');
if (tokenInput) tokenInput.value = '';
}
if (authType !== 'basic') {
if (usernameInput) usernameInput.value = '';
if (passwordInput) passwordInput.value = '';
}
if (authType !== 'bearer' && bearerTokenInput) bearerTokenInput.value = '';
if (authType !== 'basic' && usernameInput) usernameInput.value = '';
if (authType !== 'basic' && passwordInput) passwordInput.value = '';
}
function toggleFetchReferenceBundles() {
if (validateReferencesCheckbox && fetchReferenceBundlesGroup) {
fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none';
@ -257,40 +248,44 @@ document.addEventListener('DOMContentLoaded', () => {
console.warn("Could not find checkbox elements needed for toggling.");
}
}
// --- Event Listeners ---
bundleSourceRadios.forEach(radio => {
radio.addEventListener('change', updateBundleSourceUI);
});
toggleServerButton.addEventListener('click', () => {
if (appMode === 'lite') return;
useLocalHapi = !useLocalHapi;
if (useLocalHapi) fhirServerUrlInput.value = '';
updateServerToggleUI();
updateAuthInputsUI();
resourceTypesDiv.style.display = 'none';
fetchedMetadataCache = null;
console.log(`Server toggled: useLocalHapi=${useLocalHapi}`);
});
if (authTypeSelect) {
authTypeSelect.addEventListener('change', updateAuthInputsUI);
}
if (validateReferencesCheckbox) {
validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles);
} else {
console.warn("Validate references checkbox not found.");
}
fetchMetadataButton.addEventListener('click', async () => {
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
resourceTypesDiv.style.display = 'block';
let customUrl = null;
const customUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
if (!useLocalHapi && !customUrl) {
fhirServerUrlInput.classList.add('is-invalid');
alert('Please enter a valid FHIR server URL.');
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
return;
}
if (!useLocalHapi) {
customUrl = fhirServerUrlInput.value.trim().replace(/\/+$/, ''); // Normalize custom URL
if (!customUrl) {
fhirServerUrlInput.classList.add('is-invalid');
alert('Please enter a valid FHIR server URL.');
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
return;
}
try { new URL(customUrl); } catch (_) {
fhirServerUrlInput.classList.add('is-invalid');
alert('Invalid custom URL format.');
@ -299,32 +294,32 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
fhirServerUrlInput.classList.remove('is-invalid');
fetchMetadataButton.disabled = true;
fetchMetadataButton.textContent = 'Fetching...';
try {
// Build URL and headers based on the toggle state
let fetchUrl;
const fetchUrl = useLocalHapi ? '/fhir/metadata' : `${customUrl}/metadata`;
const headers = { 'Accept': 'application/fhir+json' };
if (useLocalHapi) {
if (!localHapiBaseUrl) {
throw new Error("Local HAPI URL not available. Please refresh.");
if (!useLocalHapi && customUrl) {
if (authTypeSelect && authTypeSelect.value !== 'none') {
const authType = authTypeSelect.value;
if (authType === 'bearer' && bearerTokenInput && bearerTokenInput.value) {
headers['Authorization'] = `Bearer ${bearerTokenInput.value}`;
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
headers['Authorization'] = `Basic ${credentials}`;
}
}
fetchUrl = `${localHapiBaseUrl}/metadata`; // localHapiBaseUrl is already normalized
console.log(`Fetching metadata directly from local URL: ${fetchUrl}`);
console.log(`Fetching metadata directly from: ${customUrl}`);
} else {
fetchUrl = `${customUrl}/metadata`;
// Add authentication headers for the direct request to the custom server
const authType = authTypeSelect.value;
if (authType === 'bearer' && document.getElementById('bearerToken') && document.getElementById('bearerToken').value) {
headers['Authorization'] = `Bearer ${document.getElementById('bearerToken').value}`;
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
headers['Authorization'] = `Basic ${credentials}`;
}
console.log(`Fetching metadata directly from custom URL: ${fetchUrl}`);
console.log("Fetching metadata from local HAPI server via proxy");
}
console.log(`Fetch URL: ${fetchUrl}`);
console.log(`Request Headers sent: ${JSON.stringify(headers)}`);
const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
if (!response.ok) {
let errorText = `Request failed with status ${response.status} ${response.statusText}`;
try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {}
@ -334,6 +329,7 @@ document.addEventListener('DOMContentLoaded', () => {
const data = await response.json();
console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500));
fetchedMetadataCache = data;
const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || [];
resourceButtonsContainer.innerHTML = '';
if (!resourceTypes.length) {
@ -364,6 +360,7 @@ document.addEventListener('DOMContentLoaded', () => {
fetchMetadataButton.textContent = 'Fetch Metadata';
}
});
retrieveForm.addEventListener('submit', async (e) => {
e.preventDefault();
const spinner = retrieveButton.querySelector('.spinner-border');
@ -374,6 +371,7 @@ document.addEventListener('DOMContentLoaded', () => {
downloadRetrieveButton.style.display = 'none';
retrieveZipPath = null;
retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`;
const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource);
if (!selectedResources.length) {
alert('Please select at least one resource type.');
@ -382,20 +380,31 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block';
return;
}
const currentFhirServerUrl = useLocalHapi ? localHapiBaseUrl : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
if (!currentFhirServerUrl) {
alert('FHIR Server URL is required.');
const currentFhirServerUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim();
if (!useLocalHapi && !currentFhirServerUrl) {
alert('Custom FHIR Server URL is required.');
fhirServerUrlInput.classList.add('is-invalid');
retrieveButton.disabled = false;
if (spinner) spinner.style.display = 'none';
if (icon) icon.style.display = 'inline-block';
return;
}
const formData = new FormData();
const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]');
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
selectedResources.forEach(res => formData.append('resources', res));
formData.append('fhir_server_url', currentFhirServerUrl);
// --- FIX APPLIED HERE ---
if (!useLocalHapi && currentFhirServerUrl) {
formData.append('fhir_server_url', currentFhirServerUrl);
} else {
// Explicitly use the proxy URL if local HAPI is selected
formData.append('fhir_server_url', '/fhir');
}
// --- END FIX ---
if (validateReferencesCheckbox) {
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
}
@ -408,19 +417,19 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
formData.append('fetch_reference_bundles', 'false');
}
if (!useLocalHapi && authTypeSelect) {
const authType = authTypeSelect.value;
formData.append('auth_type', authType);
if (authType === 'bearer') {
const bearerToken = document.getElementById('bearerToken').value.trim();
if (!bearerToken) {
if (authType === 'bearer' && bearerTokenInput) {
if (!bearerTokenInput.value) {
alert('Please enter a Bearer Token.');
retrieveButton.disabled = false;
if (spinner) spinner.style.display = 'none';
if (icon) icon.style.display = 'inline-block';
return;
}
formData.append('bearer_token', bearerToken);
formData.append('bearer_token', bearerTokenInput.value);
} else if (authType === 'basic' && usernameInput && passwordInput) {
if (!usernameInput.value || !passwordInput.value) {
alert('Please enter both Username and Password for Basic Authentication.');
@ -433,20 +442,24 @@ document.addEventListener('DOMContentLoaded', () => {
formData.append('password', passwordInput.value);
}
}
const headers = {
'Accept': 'application/x-ndjson',
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
'X-API-Key': apiKey
};
console.log(`Submitting retrieve request. Server: ${currentFhirServerUrl}, ValidateRefs: ${formData.get('validate_references')}, FetchRefBundles: ${formData.get('fetch_reference_bundles')}, AuthType: ${formData.get('auth_type')}`);
try {
const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' }));
throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`);
}
retrieveZipPath = response.headers.get('X-Zip-Path');
console.log(`Received X-Zip-Path: ${retrieveZipPath}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
@ -468,11 +481,13 @@ document.addEventListener('DOMContentLoaded', () => {
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
const messageDiv = document.createElement('div');
messageDiv.className = messageClass;
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
retrieveConsole.appendChild(messageDiv);
retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
if (data.type === 'complete' && retrieveZipPath) {
const completeData = data.data || {};
const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0);
@ -490,6 +505,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
if (buffer.trim()) { /* Handle final buffer */ }
} catch (e) {
console.error('Retrieval error:', e);
const ts = new Date().toLocaleTimeString();
@ -501,17 +517,20 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block';
}
});
downloadRetrieveButton.addEventListener('click', () => {
if (retrieveZipPath) {
const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip';
const downloadUrl = `/tmp/${filename}`;
console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = 'retrieved_bundles.zip';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || '';
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
@ -529,6 +548,7 @@ document.addEventListener('DOMContentLoaded', () => {
alert("Download error: No file path available.");
}
});
splitForm.addEventListener('submit', async (e) => {
e.preventDefault();
const spinner = splitButton.querySelector('.spinner-border');
@ -539,10 +559,13 @@ document.addEventListener('DOMContentLoaded', () => {
downloadSplitButton.style.display = 'none';
splitZipPath = null;
splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`;
const formData = new FormData();
const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]');
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
if (bundleSource === 'upload') {
if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) {
alert('Please select a ZIP file to upload for splitting.');
@ -574,13 +597,16 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block';
return;
}
const headers = {
'Accept': 'application/x-ndjson',
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
'X-API-Key': apiKey
};
try {
const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData });
if (!response.ok) {
const errorMsg = await response.text();
let detail = errorMsg;
@ -589,6 +615,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
splitZipPath = response.headers.get('X-Zip-Path');
console.log(`Received X-Zip-Path for split: ${splitZipPath}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
@ -621,6 +648,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
if (buffer.trim()) { /* Handle final buffer */ }
} catch (e) {
console.error('Splitting error:', e);
const ts = new Date().toLocaleTimeString();
@ -632,6 +660,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (icon) icon.style.display = 'inline-block';
}
});
downloadSplitButton.addEventListener('click', () => {
if (splitZipPath) {
const filename = splitZipPath.split('/').pop() || 'split_resources.zip';
@ -656,9 +685,10 @@ document.addEventListener('DOMContentLoaded', () => {
alert("Download error: No file path available.");
}
});
// --- Initial Setup Calls ---
updateBundleSourceUI();
fetchLocalUrlAndSetUI();
updateServerToggleUI();
toggleFetchReferenceBundles();
});
</script>

View File

@ -22,9 +22,8 @@
<div class="card">
<div class="card-header">Validation Form</div>
<div class="card-body">
<form id="validationForm" method="POST" action="/api/validate">
{{ form.csrf_token }}
<form id="validationForm">
{{ form.hidden_tag() }}
<div class="mb-3">
<small class="form-text text-muted">
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
@ -47,7 +46,6 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
{{ form.version(class="form-control", id=form.version.id) }}
@ -55,111 +53,36 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<hr class="my-4">
<!-- New Fields from ValidationForm -->
<div class="mb-3">
<label for="fhir_resource" class="form-label">FHIR Resource (JSON or XML)</label>
{{ form.fhir_resource(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
</div>
<div class="row g-3">
<div class="col-md-6">
<label for="fhir_version" class="form-label">FHIR Version</label>
{{ form.fhir_version(class="form-select") }}
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
<div class="form-check">
{{ form.include_dependencies(class="form-check-input") }}
{{ form.include_dependencies.label(class="form-check-label") }}
</div>
</div>
<hr class="my-4">
<h5>Validation Flags</h5>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check">
{{ form.do_native(class="form-check-input") }}
<label class="form-check-label" for="do_native">{{ form.do_native.label }}</label>
</div>
<div class="form-check">
{{ form.hint_about_must_support(class="form-check-input") }}
<label class="form-check-label" for="hint_about_must_support">{{ form.hint_about_must_support.label }}</label>
</div>
<div class="form-check">
{{ form.assume_valid_rest_references(class="form-check-input") }}
<label class="form-check-label" for="assume_valid_rest_references">{{ form.assume_valid_rest_references.label }}</label>
</div>
<div class="form-check">
{{ form.no_extensible_binding_warnings(class="form-check-input") }}
<label class="form-check-label" for="no_extensible_binding_warnings">{{ form.no_extensible_binding_warnings.label }}</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
{{ form.show_times(class="form-check-input") }}
<label class="form-check-label" for="show_times">{{ form.show_times.label }}</label>
</div>
<div class="form-check">
{{ form.allow_example_urls(class="form-check-input") }}
<label class="form-check-label" for="allow_example_urls">{{ form.allow_example_urls.label }}</label>
</div>
<div class="form-check">
{{ form.check_ips_codes(class="form-check-input") }}
<label class="form-check-label" for="check_ips_codes">{{ form.check_ips_codes.label }}</label>
</div>
<div class="form-check">
{{ form.allow_any_extensions(class="form-check-input") }}
<label class="form-check-label" for="allow_any_extensions">{{ form.allow_any_extensions.label }}</label>
</div>
<div class="form-check">
{{ form.tx_routing(class="form-check-input") }}
<label class="form-check-label" for="tx_routing">{{ form.tx_routing.label }}</label>
</div>
</div>
<div class="mb-3">
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
{{ form.mode(class="form-select") }}
</div>
<hr class="my-4">
<h5>Other Settings</h5>
<div class="row g-3">
<div class="col-md-6">
<label for="profiles" class="form-label">{{ form.profiles.label }}</label>
{{ form.profiles(class="form-control") }}
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
</div>
<div class="col-md-6">
<label for="extensions" class="form-label">{{ form.extensions.label }}</label>
{{ form.extensions(class="form-control") }}
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
</div>
<div class="col-md-6">
<label for="terminology_server" class="form-label">{{ form.terminology_server.label }}</label>
{{ form.terminology_server(class="form-control") }}
<small class="form-text text-muted">e.g., http://tx.fhir.org</small>
</div>
<div class="col-md-6">
<label for="snomed_ct_version" class="form-label">{{ form.snomed_ct_version.label }}</label>
{{ form.snomed_ct_version(class="form-select") }}
<small class="form-text text-muted">Select a SNOMED CT edition.</small>
</div>
<div class="mb-3">
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
{% for error in form.sample_input.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="mt-4 text-center">
<button type="submit" id="validateButton" class="btn btn-primary btn-lg">
Validate Resource
</button>
<button type="button" id="clearResultsButton" class="btn btn-secondary btn-lg d-none">
Clear Results
</button>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
<button type="button" class="btn btn-secondary d-none" id="clearResultsButton">Clear Results</button>
</div>
</form>
</div>
</div>
<div id="validationResult" class="card mt-4" style="display: none;">
<div class="card-header">Validation Results</div>
<div class="card mt-4" id="validationResult" style="display: none;">
<div class="card-header">Validation Report</div>
<div class="card-body" id="validationContent">
<!-- Results will be displayed here -->
</div>
</div>
</div>
</div>
@ -171,7 +94,6 @@
const spinnerOverlay = document.getElementById('spinner-overlay');
const spinnerMessage = document.getElementById('spinner-message');
const spinnerContainer = document.getElementById('spinner-animation');
const perPage = 10;
// Function to load the Lottie animation based on theme
function loadLottieAnimation(theme) {
@ -214,18 +136,17 @@
// --- Frontend logic starts here
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('validationForm');
const validateButton = document.getElementById('validateButton');
const clearResultsButton = document.getElementById('clearResultsButton');
const validationResult = document.getElementById('validationResult');
const validationContent = document.getElementById('validationContent');
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
const packageSelect = document.getElementById('packageSelect');
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
const versionInput = document.getElementById('{{ form.version.id }}');
const fhirResourceInput = document.getElementById('fhir_resource');
// Function to update package fields when a package is selected from the dropdown
const validateButton = document.getElementById('validateButton');
const clearResultsButton = document.getElementById('clearResultsButton');
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
const validationResult = document.getElementById('validationResult');
const validationContent = document.getElementById('validationContent');
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
const perPage = 10;
function updatePackageFields(select) {
const value = select.value;
if (!packageNameInput || !versionInput) return;
@ -243,8 +164,38 @@
versionInput.value = '';
}
}
// Sanitize text to prevent XSS
if (packageNameInput.value && versionInput.value) {
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
packageSelect.value = currentValue;
}
}
if (packageSelect) {
packageSelect.addEventListener('change', function() {
updatePackageFields(this);
});
}
if (sampleInput) {
sampleInput.addEventListener('input', function() {
try {
if (this.value.trim()) {
JSON.parse(this.value);
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid', 'is-invalid');
}
} catch (e) {
this.classList.remove('is-valid');
this.classList.add('is-invalid');
}
});
}
function sanitizeText(text) {
const div = document.createElement('div');
div.textContent = text;
@ -275,7 +226,7 @@
li.innerHTML = `
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
</div>
`;
ul.appendChild(li);
@ -295,7 +246,7 @@
li.innerHTML = `
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
</div>
`;
remainingUl.appendChild(li);
@ -324,7 +275,6 @@
clearResultsButton.classList.remove('d-none');
if (data.file_paths) {
// Use the paths returned from the server to create download links
const jsonFileName = data.file_paths.json.split('/').pop();
const htmlFileName = data.file_paths.html.split('/').pop();
const downloadDiv = document.createElement('div');
@ -413,7 +363,7 @@
new bootstrap.Tooltip(el);
});
}
function checkValidationStatus(taskId) {
fetch(`/api/check-validation-status/${taskId}`)
.then(response => {
@ -447,11 +397,10 @@
});
}
function runBlockingValidation(e) {
e.preventDefault(); // This is the crucial line to prevent default form submission
const packageName = document.getElementById('{{ form.package_name.id }}').value;
const version = document.getElementById('{{ form.version.id }}').value;
const sampleData = document.getElementById('fhir_resource').value.trim();
function runBlockingValidation() {
const packageName = packageNameInput.value;
const version = versionInput.value;
const sampleData = sampleInput.value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!packageName || !version) {
@ -462,40 +411,31 @@
if (!sampleData) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON or XML.</div>';
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
return;
}
try {
JSON.parse(sampleData);
} catch (e) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
return;
}
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
// Collect all form data including the new fields
const data = {
package_name: packageName,
version: version,
fhir_resource: sampleData,
fhir_version: document.getElementById('fhir_version').value,
do_native: document.getElementById('do_native').checked,
hint_about_must_support: document.getElementById('hint_about_must_support').checked,
assume_valid_rest_references: document.getElementById('assume_valid_rest_references').checked,
no_extensible_binding_warnings: document.getElementById('no_extensible_binding_warnings').checked,
show_times: document.getElementById('show_times').checked,
allow_example_urls: document.getElementById('allow_example_urls').checked,
check_ips_codes: document.getElementById('check_ips_codes').checked,
allow_any_extensions: document.getElementById('allow_any_extensions').checked,
tx_routing: document.getElementById('tx_routing').checked,
snomed_ct_version: document.getElementById('snomed_ct_version').value,
profiles: document.getElementById('profiles').value,
extensions: document.getElementById('extensions').value,
terminology_server: document.getElementById('terminology_server').value
};
fetch('/api/validate', {
fetch('/api/validate-sample', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data)
body: JSON.stringify({
package_name: packageName,
version: version,
sample_data: sampleData
})
})
.then(response => {
if (response.status === 202) {
@ -521,9 +461,9 @@
toggleSpinner(false);
console.error('Validation failed:', error);
validationResult.style.display = 'block';
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred: ${sanitizeText(error.message)}</div>`;
});
};
}
clearResultsButton.addEventListener('click', function() {
fetch('/api/clear-validation-results', {
@ -546,40 +486,9 @@
});
});
if (packageSelect) {
packageSelect.addEventListener('change', function() {
updatePackageFields(this);
});
}
if (validateButton) {
validateButton.addEventListener('click', function(e) {
runBlockingValidation(e);
});
}
if (packageNameInput.value && versionInput.value) {
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
packageSelect.value = currentValue;
}
}
if (fhirResourceInput) {
fhirResourceInput.addEventListener('input', function() {
try {
if (this.value.trim()) {
JSON.parse(this.value);
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid', 'is-invalid');
}
} catch (e) {
this.classList.remove('is-valid');
this.classList.add('is-invalid');
}
validateButton.addEventListener('click', function() {
runBlockingValidation();
});
}
});