hapi-fhir-jpaserver-starter

hapi-fhir-jpaserver-starter là một dự án mẫu (template project) được phát triển bởi đội ngũ HAPI FHIR, cung cấp một FHIR server hoàn chỉnh, sẵn sàng để triển khai với cấu hình tối thiểu. Đây là điểm khởi đầu lý tưởng cho bất kỳ tổ chức nào muốn nhanh chóng thiết lập một FHIR server với đầy đủ tính năng, từ các dự án thử nghiệm đến môi trường sản xuất.

Khác với các thư viện như hapi-fhir-base hay hapi-fhir-jpaserver-base cung cấp các components riêng lẻ, hapi-fhir-jpaserver-starter kết hợp tất cả các thành phần cần thiết vào một ứng dụng hoàn chỉnh, được cấu hình sẵn, và có thể mở rộng theo nhu cầu cụ thể.

Tính năng chính

1. Cấu trúc dự án đầy đủ

hapi-fhir-jpaserver-starter cung cấp một cấu trúc dự án hoàn chỉnh bao gồm:

  • Web Application: Cấu hình servlet và web components

  • JPA Configuration: Cấu hình cho persistence layer

  • FHIR Resources: Hỗ trợ tất cả các resource types của FHIR R5

  • Testing Framework: Cấu trúc và mẫu cho unit và integration tests

  • Documentation: Tài liệu và hướng dẫn triển khai

  • Build Scripts: Maven/Gradle configuration và Docker support

2. RESTful API đầy đủ

Server cung cấp triển khai hoàn chỉnh của FHIR RESTful API:

  • CRUD Operations: Create, Read, Update, Delete cho tất cả resource types

  • Search: Tìm kiếm nâng cao với tất cả search parameters theo chuẩn

  • History & Versioning: Quản lý và truy vấn lịch sử thay đổi

  • Transactions & Batches: Xử lý nhiều operations trong một request

  • Extended Operations: Hỗ trợ operations như $everything, $validate, và các operations tùy chỉnh

  • Compartments: Truy vấn resources trong các compartments

  • Patch: Hỗ trợ JSON Patch và XML Patch

3. Multi-Version Support

Một trong những đặc điểm nổi bật của HAPI FHIR JPA Server Starter là khả năng hỗ trợ nhiều phiên bản FHIR cùng một lúc:

  • R5 (5.0.0): Phiên bản mới nhất của FHIR

  • R4 (4.0.1): Phiên bản FHIR được sử dụng phổ biến hiện nay

  • R4B (4.3.0): Phiên bản cập nhật của R4

  • R3 (STU3): Phiên bản legacy vẫn được sử dụng rộng rãi

  • R2 (DSTU2): Hỗ trợ cho các hệ thống cũ

Mỗi phiên bản được triển khai tại một endpoint riêng, cho phép các ứng dụng client sử dụng phiên bản FHIR phù hợp với nhu cầu của họ.

4. Persistence Layer

HAPI FHIR JPA Server Starter bao gồm persistence layer đầy đủ:

  • Database Support: Hỗ trợ nhiều RDBMS như PostgreSQL, MySQL, Oracle, H2

  • Connection Pooling: Cấu hình HikariCP tối ưu

  • Schema Management: Tự động tạo và cập nhật schema

  • JPA Entities: Các entities đầy đủ cho tất cả resource types

  • Hibernate Configuration: Cấu hình tối ưu cho Hibernate/JPA

5. Tính năng doanh nghiệp

HAPI FHIR JPA Server Starter bao gồm nhiều tính năng doanh nghiệp:

  • Interceptor Framework: Mở rộng Server với custom business logic

  • Authorization: Tích hợp với SMART on FHIR và các frameworks authentication khác

  • Auditing: Tracking đầy đủ các thay đổi và truy cập

  • Validation: Validation resources dựa trên profiles và business rules

  • Subscriptions: Gửi notifications khi có thay đổi dữ liệu

  • Terminology Services: Quản lý và sử dụng terminologies

  • Bulk Data Access: Xuất và nhập dữ liệu số lượng lớn

  • MDM (Master Data Management): Tính năng MDM tích hợp

6. Web UI tích hợp

Server đi kèm với một web UI đơn giản nhưng mạnh mẽ:

  • Resource Browser: Duyệt và tìm kiếm resources

  • Conformance Information: Xem CapabilityStatement và metadata

  • Testing Interface: Thử nghiệm các operations

  • Documentation Access: Truy cập tài liệu API

Cài đặt và Sử dụng

1. Bắt đầu với Git Clone

Cách đơn giản nhất để bắt đầu là clone dự án từ GitHub:

# Clone repository
git clone https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git

# Di chuyển vào thư mục dự án
cd hapi-fhir-jpaserver-starter

# Biên dịch và chạy với Maven
mvn clean install
mvn jetty:run

Server sẽ chạy tại http://localhost:8080/fhir theo mặc định.

2. Triển khai với Docker

HAPI FHIR JPA Server Starter cung cấp Docker image chính thức:

# Chạy server với H2 database (phát triển)
docker run -p 8080:8080 hapiproject/hapi:latest

# Hoặc sử dụng docker-compose với PostgreSQL
docker-compose up

Docker Compose configuration mẫu:

version: '3.8'
services:
  hapi-fhir-jpaserver:
    image: hapiproject/hapi:latest
    container_name: hapi-fhir-jpaserver
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/hapi
      - SPRING_DATASOURCE_USERNAME=admin
      - SPRING_DATASOURCE_PASSWORD=admin
      - SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.postgresql.Driver
      - SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT=org.hibernate.dialect.PostgreSQL95Dialect
      - HAPI_FHIR_USE_APACHE_ADDRESS_STRATEGY=true
      - HAPI_FHIR_ALLOW_EXTERNAL_REFERENCES=true
      - HAPI_FHIR_ALLOW_PLACEHOLDER_REFERENCES=true
      - HAPI_FHIR_SUBSCRIPTION_WEBSOCKET_ENABLED=true
    depends_on:
      - db
  
  db:
    image: postgres:15-alpine
    container_name: hapi-postgres
    restart: always
    environment:
      POSTGRES_PASSWORD: admin
      POSTGRES_USER: admin
      POSTGRES_DB: hapi
    volumes:
      - hapi-postgres-data:/var/lib/postgresql/data

volumes:
  hapi-postgres-data:

3. Triển khai trên Kubernetes

HAPI FHIR JPA Server Starter có thể dễ dàng triển khai trên Kubernetes:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hapi-fhir-jpaserver
  labels:
    app: hapi-fhir-jpaserver
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hapi-fhir-jpaserver
  template:
    metadata:
      labels:
        app: hapi-fhir-jpaserver
    spec:
      containers:
      - name: hapi-fhir-jpaserver
        image: hapiproject/hapi:latest
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_DATASOURCE_URL
          value: jdbc:postgresql://postgres-service:5432/hapi
        - name: SPRING_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: postgres-credentials
              key: username
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-credentials
              key: password
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /fhir/metadata
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /fhir/metadata
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: hapi-fhir-jpaserver-service
spec:
  selector:
    app: hapi-fhir-jpaserver
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP

Tùy chỉnh và Mở rộng

1. Cấu hình Application Properties

HAPI FHIR JPA Server Starter sử dụng Spring Boot properties để cấu hình. Các thuộc tính chính có thể được cấu hình trong file application.yaml:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/hapi
    username: admin
    password: admin
    driverClassName: org.postgresql.Driver
    max-active: 20
    max-idle: 10
    max-wait: -1
  jpa:
    properties:
      hibernate.dialect: org.hibernate.dialect.PostgreSQL95Dialect
      hibernate.search.enabled: true

hapi:
  fhir:
    version: R5
    server_address: http://hapi.fhir.org/baseR5
    validation:
      enabled: true
      request_validator: ca.uhn.fhir.rest.server.interceptor.FhirPathValidationInterceptor
    narrative:
      enabled: true
    subscription:
      resthook_enabled: true
      websocket_enabled: true
      email_enabled: false
    cors:
      enabled: true
      allowed_origin: "*"
    implementation_guides:
      - org.hl7.fhir.us.core:5.0.1
    mdm_enabled: true
    allow_external_references: true
    allow_placeholder_references: true
    reuse_cached_search_results_millis: 60000
    retain_cached_searches_mins: 60
    default_page_size: 20
    max_page_size: 200
    client_id_strategy: ANY

2. Tùy chỉnh Interceptors

Interceptors là cách mạnh mẽ để mở rộng HAPI FHIR JPA Server theo nhu cầu cụ thể. Dưới đây là một ví dụ về custom interceptor:

@Component
@Interceptor
public class CustomAuthorizationInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(CustomAuthorizationInterceptor.class);
    
    @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
    public void checkAccess(RequestDetails requestDetails, ServletRequestDetails servletRequestDetails) {
        String authHeader = requestDetails.getHeader("Authorization");
        
        // Kiểm tra access token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            logger.warn("Missing or invalid Authorization header");
            throw new AuthenticationException("Missing or invalid Authorization header");
        }
        
        String token = authHeader.substring(7);
        
        // Validate token
        if (!isValidToken(token)) {
            logger.warn("Invalid access token");
            throw new AuthenticationException("Invalid access token");
        }
        
        // Extract identity
        String userId = extractUserFromToken(token);
        
        // Store user ID in request for later use by other interceptors
        requestDetails.getUserData().put("userId", userId);
        
        // Check permissions for this operation
        if (!hasPermission(userId, requestDetails.getResourceName(), requestDetails.getRequestType())) {
            logger.warn("User {} not authorized for {} operation on {}",
                     userId, requestDetails.getRequestType(), requestDetails.getResourceName());
            throw new AuthenticationException("Not authorized");
        }
    }
    
    private boolean isValidToken(String token) {
        // Implementation of token validation
        return true; // Simplified for example
    }
    
    private String extractUserFromToken(String token) {
        // Extract user ID from token
        return "user123"; // Simplified for example
    }
    
    private boolean hasPermission(String userId, String resourceType, RestOperationTypeEnum operation) {
        // Check if user has permission for this operation on this resource type
        return true; // Simplified for example
    }
}

3. Custom Operations

HAPI FHIR JPA Server Starter cho phép thêm custom operations:

@Component
public class PatientOperationsProvider {
    
    @Autowired
    private IFhirResourceDao<Patient> patientDao;
    
    @Autowired
    private FhirContext fhirContext;
    
    /**
     * Custom operation để tìm bệnh nhân phù hợp
     * Endpoint: [base]/Patient/$find-matches
     */
    @Operation(name = "$find-matches", idempotent = true)
    public Parameters findMatches(
            @OperationParam(name = "resource") Patient inputPatient,
            @OperationParam(name = "threshold") StringType threshold,
            @OperationParam(name = "count") IntegerType count) {
        
        // Parse parameters
        double matchThreshold = threshold != null ? Double.parseDouble(threshold.getValue()) : 0.8;
        int resultCount = count != null ? count.getValue() : 10;
        
        // Thực hiện logic tìm kiếm matching
        List<MatchResult> matches = findMatchingPatients(inputPatient, matchThreshold, resultCount);
        
        // Create result Parameters
        Parameters retVal = new Parameters();
        
        // Add matches to result
        for (MatchResult match : matches) {
            Parameters.ParametersParameterComponent matchParam = retVal.addParameter();
            matchParam.setName("match");
            
            // Add the patient resource
            matchParam.addPart()
                .setName("resource")
                .setResource(match.getPatient());
            
            // Add the score
            matchParam.addPart()
                .setName("score")
                .setValue(new DecimalType(match.getScore()));
        }
        
        return retVal;
    }
    
    private List<MatchResult> findMatchingPatients(Patient patient, double threshold, int maxResults) {
        // Implementation of matching algorithm
        List<MatchResult> results = new ArrayList<>();
        
        // Example implementation - in real use case, would use more sophisticated matching
        SearchParameterMap searchMap = new SearchParameterMap();
        
        // Add search parameters based on input patient
        if (patient.hasName()) {
            HumanName name = patient.getNameFirstRep();
            if (name.hasFamily()) {
                searchMap.add(Patient.SP_FAMILY, new StringParam(name.getFamily()));
            }
            if (name.hasGiven()) {
                searchMap.add(Patient.SP_GIVEN, new StringParam(name.getGivenAsSingleString()));
            }
        }
        
        if (patient.hasBirthDate()) {
            searchMap.add(Patient.SP_BIRTHDATE, new DateParam(patient.getBirthDateElement().getValueAsString()));
        }
        
        if (patient.hasGender()) {
            searchMap.add(Patient.SP_GENDER, new TokenParam(patient.getGenderElement().getValueAsString()));
        }
        
        // Execute search
        IBundleProvider searchResults = patientDao.search(searchMap);
        
        // Process results and calculate match scores
        int numToReturn = Math.min(searchResults.size(), maxResults);
        List<IBaseResource> resources = searchResults.getResources(0, numToReturn);
        
        for (IBaseResource resource : resources) {
            Patient matchPatient = (Patient) resource;
            double score = calculateMatchScore(patient, matchPatient);
            
            if (score >= threshold) {
                results.add(new MatchResult(matchPatient, score));
            }
        }
        
        // Sort by score descending
        results.sort((a, b) -> Double.compare(b.getScore(), a.getScore()));
        
        return results.subList(0, Math.min(results.size(), maxResults));
    }
    
    private double calculateMatchScore(Patient sourcePatient, Patient targetPatient) {
        // Implementation of match scoring algorithm
        // This is a simplified example
        double score = 0.0;
        double totalWeight = 0.0;
        
        // Compare names
        if (sourcePatient.hasName() && targetPatient.hasName()) {
            HumanName sourceName = sourcePatient.getNameFirstRep();
            HumanName targetName = targetPatient.getNameFirstRep();
            
            if (sourceName.hasFamily() && targetName.hasFamily()) {
                double nameScore = calculateStringSimilarity(
                    sourceName.getFamily(), targetName.getFamily());
                score += nameScore * 0.3;
                totalWeight += 0.3;
            }
            
            if (sourceName.hasGiven() && targetName.hasGiven()) {
                double givenScore = calculateStringSimilarity(
                    sourceName.getGivenAsSingleString(), targetName.getGivenAsSingleString());
                score += givenScore * 0.2;
                totalWeight += 0.2;
            }
        }
        
        // Compare birth dates
        if (sourcePatient.hasBirthDate() && targetPatient.hasBirthDate()) {
            boolean datesMatch = sourcePatient.getBirthDate().equals(targetPatient.getBirthDate());
            if (datesMatch) {
                score += 0.3;
            }
            totalWeight += 0.3;
        }
        
        // Compare gender
        if (sourcePatient.hasGender() && targetPatient.hasGender()) {
            boolean genderMatches = sourcePatient.getGender().equals(targetPatient.getGender());
            if (genderMatches) {
                score += 0.2;
            }
            totalWeight += 0.2;
        }
        
        // Normalize score
        return totalWeight > 0 ? score / totalWeight : 0;
    }
    
    private double calculateStringSimilarity(String s1, String s2) {
        // Simple implementation using Levenshtein distance
        int distance = levenshteinDistance(s1.toLowerCase(), s2.toLowerCase());
        int maxLength = Math.max(s1.length(), s2.length());
        
        return maxLength > 0 ? 1.0 - (double) distance / maxLength : 1.0;
    }
    
    private int levenshteinDistance(String s1, String s2) {
        // Implementation of Levenshtein distance
        // ...
        return 0; // Simplified for example
    }
    
    // Helper class to store match results
    private static class MatchResult {
        private final Patient patient;
        private final double score;
        
        public MatchResult(Patient patient, double score) {
            this.patient = patient;
            this.score = score;
        }
        
        public Patient getPatient() {
            return patient;
        }
        
        public double getScore() {
            return score;
        }
    }
}

4. Tích hợp với Spring Boot

HAPI FHIR JPA Server Starter có thể được sử dụng như một ứng dụng Spring Boot đầy đủ:

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    /**
     * Customize the FHIR server configuration
     */
    @Bean
    public DaoConfig daoConfig() {
        DaoConfig config = new DaoConfig();
        config.setAllowExternalReferences(true);
        config.setReuseCachedSearchResultsForMillis(60000);
        config.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID);
        config.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.ANY);
        config.setDefaultSearchParamsCanBeOverridden(true);
        config.setExpungeEnabled(true);
        
        // Enable MDM
        config.setEnableInMemorySubscriptionMatching(true);
        
        return config;
    }
    
    /**
     * Customize the JPA configuration
     */
    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager manager = new JpaTransactionManager();
        manager.setEntityManagerFactory(entityManagerFactory);
        return manager;
    }
    
    /**
     * Add custom authentication
     */
    @Bean
    public IServerInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
    
    /**
     * Configure CORS
     */
    @Bean
    public CorsInterceptor corsInterceptor() {
        CorsInterceptor interceptor = new CorsInterceptor();
        
        // Configure CORS
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.setAllowCredentials(true);
        
        interceptor.setConfig(config);
        
        return interceptor;
    }
}

Best Practices

1. Production Deployment

Khi triển khai HAPI FHIR JPA Server Starter trong môi trường sản xuất, hãy cân nhắc các best practices sau:

  1. Sử dụng database enterprise-grade: PostgreSQL hoặc Oracle thay vì H2 cho production

  2. Cấu hình connection pool: Tối ưu hóa HikariCP cho workload của bạn

  3. Enable caching: Bật và cấu hình caching phù hợp

  4. Cấu hình memory: Đảm bảo đủ heap memory cho Hibernate và ứng dụng

  5. Bảo mật: Thêm authentication và authorization controls

  6. HTTPS: Luôn sử dụng TLS trong production

  7. Monitoring: Thiết lập giám sát và alerting

  8. Backup strategy: Cấu hình backup thường xuyên cho database

2. Performance Tuning

# application.yaml với cấu hình hiệu suất cao
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/hapi
    username: admin
    password: admin
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10
      idle-timeout: 60000
      connection-timeout: 30000
  jpa:
    properties:
      hibernate.dialect: org.hibernate.dialect.PostgreSQL95Dialect
      hibernate.search.backend.type: lucene
      hibernate.search.backend.directory.type: local-filesystem
      hibernate.search.backend.directory.root: /var/lib/hapi/lucene
      hibernate.search.backend.lucene_version: LATEST
      hibernate.jdbc.batch_size: 20
      hibernate.default_batch_fetch_size: 20
      hibernate.connection.provider_disables_autocommit: true
      hibernate.query.plan_cache_max_size: 2048
      hibernate.query.plan_parameter_metadata_max_size: 128
      hibernate.cache.use_second_level_cache: true
      hibernate.cache.use_query_cache: true
      hibernate.cache.region.factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
      hibernate.javax.cache.provider: org.ehcache.jsr107.EhcacheCachingProvider

hapi:
  fhir:
    tester:
      enabled: false
    validation:
      enabled: false
    narrative:
      enabled: false
    advanced_lucene_indexing: true
    store_resource_in_lucene_index: true
    reuse_cached_search_results_millis: 3600000
    retain_cached_searches_mins: 60
    default_page_size: 100
    max_page_size: 1000
    expunge_enabled: true
    subscription:
      resthook_enabled: true
      websocket_enabled: true
      email_enabled: false
    local_base_urls:
      - http://hapi.example.com/fhir
    defer_indexing_for_codesystems_of_size: 100
    install_transitive_ig_dependencies: false

3. Security Configuration

HAPI FHIR JPA Server Starter có thể được bảo mật với OAuth2 và SMART on FHIR:

@Configuration
public class SecurityConfig {
    
    @Autowired
    private Environment env;
    
    @Bean
    public SmartServerCapabilityStatementInterceptor smartServerCapabilityStatementInterceptor() {
        SmartServerCapabilityStatementInterceptor interceptor = new SmartServerCapabilityStatementInterceptor();
        
        // Configure SMART capabilities
        interceptor.setAuthorizationEndpoint(env.getProperty("smart.auth_url"));
        interceptor.setTokenEndpoint(env.getProperty("smart.token_url"));
        interceptor.setRegisterEndpoint(env.getProperty("smart.register_url"));
        interceptor.setManagementEndpoint(env.getProperty("smart.management_url"));
        
        List<SmartCapabilityStatement.RestSecurityService> services = new ArrayList<>();
        services.add(SmartCapabilityStatement.RestSecurityService.SMART_ON_FHIR);
        services.add(SmartCapabilityStatement.RestSecurityService.LAUNCH_STANDALONE);
        services.add(SmartCapabilityStatement.RestSecurityService.CONTEXT_STANDALONE_PATIENT);
        interceptor.setRestSecurityServices(services);
        
        return interceptor;
    }
    
    @Bean
    public OAuthInterceptor oauthInterceptor() {
        OAuthInterceptor interceptor = new OAuthInterceptor();
        
        // Configure OAuth validation
        interceptor.setAuthorizationServerUrl(env.getProperty("oauth.server_url"));
        interceptor.setIntrospectionUrl(env.getProperty("oauth.introspection_url"));
        interceptor.setClientId(env.getProperty("oauth.client_id"));
        interceptor.setClientSecret(env.getProperty("oauth.client_secret"));
        
        // Configure scope mapping
        Map<String, List<String>> scopeMap = new HashMap<>();
        scopeMap.put("patient/*.read", Arrays.asList("Patient", "Observation", "Condition", "MedicationRequest"));
        scopeMap.put("user/*.read", Arrays.asList("Patient", "Observation", "Condition", "MedicationRequest"));
        scopeMap.put("patient/*.write", Arrays.asList("Patient", "Observation", "Condition", "MedicationRequest"));
        
        interceptor.setScopeMap(scopeMap);
        
        return interceptor;
    }
}

Ví dụ thực tế

1. Clinical Data Repository

HAPI FHIR JPA Server Starter có thể được sử dụng làm Clinical Data Repository:

@Configuration
public class ClinicalRepositoryConfig {
    
    @Bean
    public DaoConfig daoConfig() {
        DaoConfig config = new DaoConfig();
        
        // Enable versioning
        config.setResourceVersioningEnabled(true);
        
        // Set version policies
        config.setDeleteEnabled(false);
        config.setExpungeEnabled(false);
        
        // Configure validation
        config.setValidateResourcesForStorage(true);
        config.setEnforceReferentialIntegrityOnWrite(true);
        config.setEnforceReferentialIntegrityOnDelete(true);
        
        // Configure patient compartment
        config.setEnforceReferentialIntegrityOnDeleteByDefault(true);
        
        return config;
    }
    
    @Bean
    public ValidationSupportChain validationSupport(FhirContext fhirContext) {
        // Configure validation support with standard resources and custom profiles
        DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(fhirContext);
        
        // Add US Core profiles
        NpmPackageValidationSupport npmSupport = new NpmPackageValidationSupport(fhirContext);
        npmSupport.loadPackageFromClasspath("org.hl7.fhir.us.core#5.0.1");
        
        // Create pre-populated validation support
        PrePopulatedValidationSupport prePopulatedSupport = new PrePopulatedValidationSupport(fhirContext);
        
        // Add custom StructureDefinitions
        StructureDefinition myPatientProfile = loadStructureDefinition("/profiles/MyPatientProfile.json");
        prePopulatedSupport.addStructureDefinition(myPatientProfile);
        
        // Combine all validation supports
        ValidationSupportChain validationSupportChain = new ValidationSupportChain(
            defaultSupport, 
            npmSupport, 
            prePopulatedSupport
        );
        
        return validationSupportChain;
    }
    
    private StructureDefinition loadStructureDefinition(String path) {
        try {
            InputStream is = getClass().getResourceAsStream(path);
            FhirContext ctx = FhirContext.forR5();
            return (StructureDefinition) ctx.newJsonParser().parseResource(is);
        } catch (Exception e) {
            throw new RuntimeException("Failed to load StructureDefinition", e);
        }
    }
    
   @Bean
   public FhirInstanceValidator instanceValidator(ValidationSupportChain validationSupport) {
       FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupport);
       instanceValidator.setValidateAgainstStandardSchema(true);
       instanceValidator.setValidateAgainstStandardSchematron(true);
       instanceValidator.setAnyExtensionsAllowed(false);
       instanceValidator.setErrorForUnknownProfiles(false);
       
       return instanceValidator;
   }
   
   @Bean
   public IServerInterceptor validationInterceptor(FhirInstanceValidator instanceValidator) {
       RequestValidatingInterceptor interceptor = new RequestValidatingInterceptor();
       interceptor.addValidatorModule(instanceValidator);
       interceptor.setFailOnSeverity(ResultSeverityEnum.ERROR);
       interceptor.setAddResponseHeaderOnSeverity(ResultSeverityEnum.INFORMATION);
       interceptor.setResponseHeaderName("X-FHIR-Validation");
       
       return interceptor;
   }
}

2. Patient Portal Backend

HAPI FHIR JPA Server Starter có thể được cấu hình đặc biệt cho patient portal:

@Configuration
public class PatientPortalConfig {
    
    @Autowired
    private DaoRegistry daoRegistry;
    
    @Bean
    public PatientAccessInterceptor patientAccessInterceptor() {
        PatientAccessInterceptor interceptor = new PatientAccessInterceptor();
        return interceptor;
    }
    
    @RestController
    @RequestMapping("/api/portal")
    public class PatientPortalController {
        
        @Autowired
        private FhirContext fhirContext;
        
        @Autowired
        private IGenericClient fhirClient;
        
        @GetMapping("/patient/{id}/summary")
        public PatientSummaryDTO getPatientSummary(@PathVariable String id, Authentication auth) {
            // Verify authorization
            verifyPatientAccess(id, auth);
            
            PatientSummaryDTO summary = new PatientSummaryDTO();
            
            // Retrieve patient
            Patient patient = fhirClient.read()
                    .resource(Patient.class)
                    .withId(id)
                    .execute();
            
            // Set basic info
            summary.setPatientId(id);
            summary.setName(getName(patient));
            summary.setBirthDate(patient.getBirthDate());
            summary.setGender(patient.getGender().getDisplay());
            
            // Get recent conditions
            Bundle conditionBundle = fhirClient.search()
                    .forResource(Condition.class)
                    .where(Condition.SUBJECT.hasId(id))
                    .sort().descending(Condition.RECORDED_DATE)
                    .count(5)
                    .returnBundle(Bundle.class)
                    .execute();
            
            List<ConditionDTO> conditions = extractConditions(conditionBundle);
            summary.setRecentConditions(conditions);
            
            // Get recent medications
            Bundle medicationBundle = fhirClient.search()
                    .forResource(MedicationRequest.class)
                    .where(MedicationRequest.SUBJECT.hasId(id))
                    .and(MedicationRequest.STATUS.exactly().code("active"))
                    .sort().descending(MedicationRequest.AUTHORED_ON)
                    .count(5)
                    .returnBundle(Bundle.class)
                    .execute();
            
            List<MedicationDTO> medications = extractMedications(medicationBundle);
            summary.setCurrentMedications(medications);
            
            // Get upcoming appointments
            Bundle appointmentBundle = fhirClient.search()
                    .forResource(Appointment.class)
                    .where(Appointment.PARTICIPANT.hasId(id))
                    .and(Appointment.DATE.afterOrEquals().now())
                    .sort().ascending(Appointment.DATE)
                    .count(5)
                    .returnBundle(Bundle.class)
                    .execute();
            
            List<AppointmentDTO> appointments = extractAppointments(appointmentBundle);
            summary.setUpcomingAppointments(appointments);
            
            return summary;
        }
        
        @PostMapping("/patient/{id}/communication")
        public ResponseEntity<CommunicationResponseDTO> sendMessage(
                @PathVariable String id,
                @RequestBody CommunicationRequestDTO request,
                Authentication auth) {
            
            // Verify authorization
            verifyPatientAccess(id, auth);
            
            // Create Communication resource
            Communication communication = new Communication();
            communication.setStatus(Communication.CommunicationStatus.INPROGRESS);
            
            // Set sender (patient)
            communication.setSender(new Reference("Patient/" + id));
            
            // Set recipient (provider)
            if (request.getRecipientId() != null) {
                communication.addRecipient(new Reference("Practitioner/" + request.getRecipientId()));
            }
            
            // Set payload
            Communication.CommunicationPayloadComponent payload = communication.addPayload();
            payload.setContent(new StringType(request.getMessage()));
            
            // Set sent time
            communication.setSent(new Date());
            
            // Set subject
            communication.setSubject(new Reference("Patient/" + id));
            
            // Set topic
            if (request.getTopic() != null) {
                CodeableConcept topic = new CodeableConcept();
                topic.setText(request.getTopic());
                communication.setTopic(topic);
            }
            
            // Save the Communication
            MethodOutcome outcome = fhirClient.create()
                    .resource(communication)
                    .execute();
            
            // Return response
            CommunicationResponseDTO response = new CommunicationResponseDTO();
            response.setId(outcome.getId().getIdPart());
            response.setStatus("sent");
            response.setTimestamp(new Date());
            
            return ResponseEntity.ok(response);
        }
        
        private void verifyPatientAccess(String patientId, Authentication auth) {
            // Implement access control logic
            String userId = ((UserDetails) auth.getPrincipal()).getUsername();
            
            // Check if user has access to this patient
            if (!hasAccess(userId, patientId)) {
                throw new AccessDeniedException("User does not have access to this patient");
            }
        }
        
        private boolean hasAccess(String userId, String patientId) {
            // Implementation of access control logic
            return true; // Simplified for example
        }
        
        private String getName(Patient patient) {
            if (patient.hasName()) {
                HumanName name = patient.getNameFirstRep();
                return name.getGivenAsSingleString() + " " + name.getFamily();
            }
            return "";
        }
        
        private List<ConditionDTO> extractConditions(Bundle bundle) {
            List<ConditionDTO> conditions = new ArrayList<>();
            for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
                Condition condition = (Condition) entry.getResource();
                ConditionDTO dto = new ConditionDTO();
                
                // Set condition properties
                dto.setId(condition.getIdElement().getIdPart());
                if (condition.hasCode() && condition.getCode().hasCoding()) {
                    Coding coding = condition.getCode().getCodingFirstRep();
                    dto.setCode(coding.getCode());
                    dto.setDisplay(coding.getDisplay());
                } else if (condition.getCode().hasText()) {
                    dto.setDisplay(condition.getCode().getText());
                }
                
                if (condition.hasRecordedDate()) {
                    dto.setRecordedDate(condition.getRecordedDate());
                }
                
                conditions.add(dto);
            }
            return conditions;
        }
        
        private List<MedicationDTO> extractMedications(Bundle bundle) {
            // Similar implementation as extractConditions
            return new ArrayList<>();
        }
        
        private List<AppointmentDTO> extractAppointments(Bundle bundle) {
            // Similar implementation as extractConditions
            return new ArrayList<>();
        }
    }
    
    // DTOs
    public static class PatientSummaryDTO {
        private String patientId;
        private String name;
        private Date birthDate;
        private String gender;
        private List<ConditionDTO> recentConditions;
        private List<MedicationDTO> currentMedications;
        private List<AppointmentDTO> upcomingAppointments;
        
        // Getters and setters
    }
    
    public static class ConditionDTO {
        private String id;
        private String code;
        private String display;
        private Date recordedDate;
        
        // Getters and setters
    }
    
    public static class MedicationDTO {
        private String id;
        private String code;
        private String display;
        private String dosage;
        private Date startDate;
        
        // Getters and setters
    }
    
    public static class AppointmentDTO {
        private String id;
        private Date date;
        private String type;
        private String practitionerName;
        private String location;
        
        // Getters and setters
    }
    
    public static class CommunicationRequestDTO {
        private String recipientId;
        private String message;
        private String topic;
        
        // Getters and setters
    }
    
    public static class CommunicationResponseDTO {
        private String id;
        private String status;
        private Date timestamp;
        
        // Getters and setters
    }
}

Hiệu suất và Scaling

HAPI FHIR JPA Server Starter có thể được điều chỉnh để xử lý workloads lớn:

@Configuration
public class PerformanceConfig {
    
    @Bean
    public DaoConfig daoConfig() {
        DaoConfig config = new DaoConfig();
        
        // Cache configuration
        config.setReuseCachedSearchResultsForMillis(600000); // 10 minutes
        config.setTranslationCachesEnabled(true);
        config.setDeferIndexingForCodesystemsOfSize(100);
        
        // Search configuration
        config.setDefaultPageSize(100);
        config.setMaximumPageSize(1000);
        config.setHardPageSize(2000);
        config.setAllowContainsSearches(true);
        config.setAllowMultipleDelete(true);
        config.setExpungeEnabled(true);
        config.setAllowExternalReferences(true);
        config.setEnforceReferentialIntegrityOnDelete(false);
        
        // MDM configuration
        config.setMdmEnabled(true);
        
        return config;
    }
    
    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("hapi-");
        executor.initialize();
        return executor;
    }
    
    @Bean
    public HikariDataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost:5432/hapi");
        config.setUsername("admin");
        config.setPassword("admin");
        config.setDriverClassName("org.postgresql.Driver");
        
        // Connection pool settings
        config.setMaximumPoolSize(50);
        config.setMinimumIdle(10);
        config.setIdleTimeout(60000);
        config.setConnectionTimeout(30000);
        config.setMaxLifetime(1800000);
        
        // Performance settings
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        config.addDataSourceProperty("useServerPrepStmts", "true");
        
        return new HikariDataSource(config);
    }
    
    @Bean
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }
    
    @Interceptor
    public static class PerformanceInterceptor {
        
        private static final Logger logger = LoggerFactory.getLogger(PerformanceInterceptor.class);
        
        private static final long SLOW_THRESHOLD_MS = 1000;
        
        @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
        public void beginRequest(ServletRequestDetails requestDetails) {
            requestDetails.setAttribute("request_start_time", System.currentTimeMillis());
        }
        
        @Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
        public void endRequest(ServletRequestDetails requestDetails, ResponseDetails responseDetails) {
            Long startTime = (Long) requestDetails.getAttribute("request_start_time");
            if (startTime != null) {
                long endTime = System.currentTimeMillis();
                long duration = endTime - startTime;
                
                if (duration > SLOW_THRESHOLD_MS) {
                    logger.warn("Slow request: {} {} - {}ms",
                              requestDetails.getRequestType(),
                              requestDetails.getCompleteUrl(),
                              duration);
                }
                
                // Add response header with timing
                responseDetails.addResponseHeader("X-Response-Time", duration + "ms");
            }
        }
        
        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
        public void resourceCreated(IBaseResource resource, ServletRequestDetails requestDetails) {
            logResourceOperation("created", resource);
        }
        
        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
        public void resourceUpdated(IBaseResource resource, ServletRequestDetails requestDetails) {
            logResourceOperation("updated", resource);
        }
        
        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
        public void resourceDeleted(IBaseResource resource, ServletRequestDetails requestDetails) {
            logResourceOperation("deleted", resource);
        }
        
        private void logResourceOperation(String operation, IBaseResource resource) {
            if (logger.isDebugEnabled()) {
                String resourceType = resource.getClass().getSimpleName();
                String resourceId = resource.getIdElement().getValue();
                logger.debug("Resource {} {}", operation, resourceType + "/" + resourceId);
            }
        }
    }
}

Monitoring và Observability

@Configuration
public class MonitoringConfig {
    
    @Bean
    public MeterRegistry meterRegistry() {
        CompositeMeterRegistry registry = new CompositeMeterRegistry();
        registry.add(new SimpleMeterRegistry());
        return registry;
    }
    
    @Bean
    public LoggingInterceptor loggingInterceptor() {
        LoggingInterceptor interceptor = new LoggingInterceptor();
        interceptor.setLogRequestHeaders(true);
        interceptor.setLogRequestBody(false);
        interceptor.setLogResponseHeaders(true);
        interceptor.setLogResponseBody(false);
        return interceptor;
    }
    
    @Bean
    public RequestCounterInterceptor requestCounterInterceptor(MeterRegistry meterRegistry) {
        return new RequestCounterInterceptor(meterRegistry);
    }
    
    @Interceptor
    public static class RequestCounterInterceptor {
        
        private final Map<String, Counter> resourceTypeCounters = new ConcurrentHashMap<>();
        private final Counter totalCounter;
        private final Map<String, Timer> resourceTypeTimers = new ConcurrentHashMap<>();
        private final Timer totalTimer;
        
        public RequestCounterInterceptor(MeterRegistry meterRegistry) {
            this.totalCounter = Counter.builder("fhir.requests.total")
                    .description("Total number of FHIR requests")
                    .register(meterRegistry);
            
            this.totalTimer = Timer.builder("fhir.requests.duration")
                    .description("Duration of FHIR requests")
                    .register(meterRegistry);
        }
        
        @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
        public void beginRequest(ServletRequestDetails requestDetails) {
            requestDetails.setAttribute("request_timer_sample", Timer.start());
            totalCounter.increment();
            
            String resourceType = requestDetails.getResourceName();
            if (resourceType != null) {
                Counter counter = resourceTypeCounters.computeIfAbsent(resourceType,
                        key -> Counter.builder("fhir.requests.byResourceType")
                                .tag("resourceType", key)
                                .register(((Counter) totalCounter).getId().getMeterRegistry()));
                counter.increment();
            }
        }
        
        @Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
        public void endRequest(ServletRequestDetails requestDetails) {
            Timer.Sample sample = (Timer.Sample) requestDetails.getAttribute("request_timer_sample");
            if (sample != null) {
                sample.stop(totalTimer);
                
                String resourceType = requestDetails.getResourceName();
                if (resourceType != null) {
                    Timer timer = resourceTypeTimers.computeIfAbsent(resourceType,
                            key -> Timer.builder("fhir.requests.duration.byResourceType")
                                    .tag("resourceType", key)
                                    .register(totalTimer.getId().getMeterRegistry()));
                    sample.stop(timer);
                }
            }
        }
    }
}

Kết luận

HAPI FHIR JPA Server Starter là một dự án mạnh mẽ giúp nhà phát triển nhanh chóng thiết lập một FHIR server đầy đủ tính năng. Với cấu trúc modulare và khả năng tùy chỉnh cao, dự án này là lựa chọn lý tưởng cho việc xây dựng các ứng dụng y tế hiện đại tuân thủ tiêu chuẩn FHIR.

Các lợi ích chính của HAPI FHIR JPA Server Starter bao gồm:

  1. Khởi đầu nhanh chóng: Từ zero đến một FHIR server hoạt động đầy đủ trong thời gian ngắn

  2. Cấu hình dễ dàng: Cấu hình đơn giản qua Spring Boot properties

  3. Đầy đủ tính năng: Triển khai toàn bộ RESTful API của FHIR R5 và các phiên bản trước đó

  4. Khả năng mở rộng: Dễ dàng thêm các custom operations và logic business

  5. Hiệu suất cao: Có thể tối ưu và mở rộng quy mô để xử lý workloads lớn

  6. Bảo trì đơn giản: Được hỗ trợ tích cực bởi cộng đồng HAPI FHIR

Cho dù bạn đang xây dựng một proof-of-concept, một ứng dụng nghiên cứu, hay một hệ thống y tế doanh nghiệp đầy đủ, HAPI FHIR JPA Server Starter cung cấp một nền tảng vững chắc cho phép bạn tập trung vào các tính năng đặc thù của ứng dụng thay vì bắt đầu từ đầu.

Last updated