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:
Sử dụng database enterprise-grade: PostgreSQL hoặc Oracle thay vì H2 cho production
Cấu hình connection pool: Tối ưu hóa HikariCP cho workload của bạn
Enable caching: Bật và cấu hình caching phù hợp
Cấu hình memory: Đảm bảo đủ heap memory cho Hibernate và ứng dụng
Bảo mật: Thêm authentication và authorization controls
HTTPS: Luôn sử dụng TLS trong production
Monitoring: Thiết lập giám sát và alerting
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:
Khởi đầu nhanh chóng: Từ zero đến một FHIR server hoạt động đầy đủ trong thời gian ngắn
Cấu hình dễ dàng: Cấu hình đơn giản qua Spring Boot properties
Đầ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 đó
Khả năng mở rộng: Dễ dàng thêm các custom operations và logic business
Hiệu suất cao: Có thể tối ưu và mở rộng quy mô để xử lý workloads lớn
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