
001/*- 002 * #%L 003 * HAPI FHIR Subscription Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.topic; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.Hook; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; 028import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator; 029import ca.uhn.fhir.parser.DataFormatException; 030import ca.uhn.fhir.rest.api.server.RequestDetails; 031import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 032import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 033import ca.uhn.fhir.util.Logs; 034import ca.uhn.hapi.converters.canonical.SubscriptionTopicCanonicalizer; 035import com.google.common.annotations.VisibleForTesting; 036import org.hl7.fhir.instance.model.api.IBaseResource; 037import org.hl7.fhir.r5.model.SubscriptionTopic; 038import org.slf4j.Logger; 039 040public class SubscriptionTopicValidatingInterceptor { 041 private static final Logger ourLog = Logs.getSubscriptionTopicLog(); 042 043 private final FhirContext myFhirContext; 044 private final SubscriptionQueryValidator mySubscriptionQueryValidator; 045 046 public SubscriptionTopicValidatingInterceptor( 047 FhirContext theFhirContext, SubscriptionQueryValidator theSubscriptionQueryValidator) { 048 myFhirContext = theFhirContext; 049 mySubscriptionQueryValidator = theSubscriptionQueryValidator; 050 } 051 052 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) 053 public void resourcePreCreate( 054 IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { 055 validateSubmittedSubscriptionTopic( 056 theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); 057 } 058 059 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) 060 public void resourceUpdated( 061 IBaseResource theOldResource, 062 IBaseResource theResource, 063 RequestDetails theRequestDetails, 064 RequestPartitionId theRequestPartitionId) { 065 validateSubmittedSubscriptionTopic( 066 theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED); 067 } 068 069 @VisibleForTesting 070 void validateSubmittedSubscriptionTopic( 071 IBaseResource theSubscription, 072 RequestDetails theRequestDetails, 073 RequestPartitionId theRequestPartitionId, 074 Pointcut thePointcut) { 075 if (Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED != thePointcut 076 && Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED != thePointcut) { 077 throw new UnprocessableEntityException(Msg.code(2340) 078 + "Expected Pointcut to be either STORAGE_PRESTORAGE_RESOURCE_CREATED or STORAGE_PRESTORAGE_RESOURCE_UPDATED but was: " 079 + thePointcut); 080 } 081 082 if (!"SubscriptionTopic".equals(myFhirContext.getResourceType(theSubscription))) { 083 return; 084 } 085 086 SubscriptionTopic subscriptionTopic = 087 SubscriptionTopicCanonicalizer.canonicalizeTopic(myFhirContext, theSubscription); 088 089 boolean finished = false; 090 if (subscriptionTopic.getStatus() == null) { 091 throw new UnprocessableEntityException( 092 Msg.code(2338) 093 + "Can not process submitted SubscriptionTopic - SubscriptionTopic.status must be populated on this server"); 094 } 095 096 switch (subscriptionTopic.getStatus()) { 097 case ACTIVE: 098 break; 099 default: 100 finished = true; 101 break; 102 } 103 104 // WIP STR5 add cross-partition support like in SubscriptionValidatingInterceptor 105 106 // WIP STR5 warn if the SubscriptionTopic criteria can't be evaluated in memory? Do we want to annotate the 107 // strategy with an extension like Subscription? 108 109 if (!finished) { 110 subscriptionTopic.getResourceTrigger().stream().forEach(t -> validateQueryCriteria(t.getQueryCriteria())); 111 } 112 } 113 114 private void validateQueryCriteria( 115 SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) { 116 if (theQueryCriteria.getPrevious() != null) { 117 validateCriteria( 118 theQueryCriteria.getPrevious(), "SubscriptionTopic.resourceTrigger.queryCriteria.previous"); 119 } 120 if (theQueryCriteria.getCurrent() != null) { 121 validateCriteria(theQueryCriteria.getCurrent(), "SubscriptionTopic.resourceTrigger.queryCriteria.current"); 122 } 123 } 124 125 public void validateCriteria(String theCriteria, String theFieldName) { 126 try { 127 mySubscriptionQueryValidator.validateCriteria(theCriteria, theFieldName); 128 SubscriptionMatchingStrategy strategy = mySubscriptionQueryValidator.determineStrategy(theCriteria); 129 if (strategy != SubscriptionMatchingStrategy.IN_MEMORY) { 130 ourLog.warn( 131 "Warning: Query Criteria '{}' in {} cannot be evaluated in-memory", theCriteria, theFieldName); 132 } 133 } catch (InvalidRequestException | DataFormatException e) { 134 throw new UnprocessableEntityException(Msg.code(2339) + "Invalid SubscriptionTopic criteria '" + theCriteria 135 + "' in " + theFieldName + ": " + e.getMessage()); 136 } 137 } 138}