001/*- 002 * #%L 003 * HAPI FHIR Subscription Server 004 * %% 005 * Copyright (C) 2014 - 2024 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 com.google.common.annotations.VisibleForTesting; 035import org.hl7.fhir.instance.model.api.IBaseResource; 036import org.hl7.fhir.r5.model.SubscriptionTopic; 037import org.slf4j.Logger; 038 039public class SubscriptionTopicValidatingInterceptor { 040 private static final Logger ourLog = Logs.getSubscriptionTopicLog(); 041 042 private final FhirContext myFhirContext; 043 private final SubscriptionQueryValidator mySubscriptionQueryValidator; 044 045 public SubscriptionTopicValidatingInterceptor( 046 FhirContext theFhirContext, SubscriptionQueryValidator theSubscriptionQueryValidator) { 047 myFhirContext = theFhirContext; 048 mySubscriptionQueryValidator = theSubscriptionQueryValidator; 049 } 050 051 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) 052 public void resourcePreCreate( 053 IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { 054 validateSubmittedSubscriptionTopic( 055 theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); 056 } 057 058 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) 059 public void resourceUpdated( 060 IBaseResource theOldResource, 061 IBaseResource theResource, 062 RequestDetails theRequestDetails, 063 RequestPartitionId theRequestPartitionId) { 064 validateSubmittedSubscriptionTopic( 065 theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED); 066 } 067 068 @VisibleForTesting 069 void validateSubmittedSubscriptionTopic( 070 IBaseResource theSubscription, 071 RequestDetails theRequestDetails, 072 RequestPartitionId theRequestPartitionId, 073 Pointcut thePointcut) { 074 if (Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED != thePointcut 075 && Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED != thePointcut) { 076 throw new UnprocessableEntityException(Msg.code(2340) 077 + "Expected Pointcut to be either STORAGE_PRESTORAGE_RESOURCE_CREATED or STORAGE_PRESTORAGE_RESOURCE_UPDATED but was: " 078 + thePointcut); 079 } 080 081 if (!"SubscriptionTopic".equals(myFhirContext.getResourceType(theSubscription))) { 082 return; 083 } 084 085 SubscriptionTopic subscriptionTopic = 086 SubscriptionTopicCanonicalizer.canonicalizeTopic(myFhirContext, theSubscription); 087 088 boolean finished = false; 089 if (subscriptionTopic.getStatus() == null) { 090 throw new UnprocessableEntityException( 091 Msg.code(2338) 092 + "Can not process submitted SubscriptionTopic - SubscriptionTopic.status must be populated on this server"); 093 } 094 095 switch (subscriptionTopic.getStatus()) { 096 case ACTIVE: 097 break; 098 default: 099 finished = true; 100 break; 101 } 102 103 // WIP STR5 add cross-partition support like in SubscriptionValidatingInterceptor 104 105 // WIP STR5 warn if the SubscriptionTopic criteria can't be evaluated in memory? Do we want to annotate the 106 // strategy with an extension like Subscription? 107 108 if (!finished) { 109 subscriptionTopic.getResourceTrigger().stream().forEach(t -> validateQueryCriteria(t.getQueryCriteria())); 110 } 111 } 112 113 private void validateQueryCriteria( 114 SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) { 115 if (theQueryCriteria.getPrevious() != null) { 116 validateCriteria( 117 theQueryCriteria.getPrevious(), "SubscriptionTopic.resourceTrigger.queryCriteria.previous"); 118 } 119 if (theQueryCriteria.getCurrent() != null) { 120 validateCriteria(theQueryCriteria.getCurrent(), "SubscriptionTopic.resourceTrigger.queryCriteria.current"); 121 } 122 } 123 124 public void validateCriteria(String theCriteria, String theFieldName) { 125 try { 126 mySubscriptionQueryValidator.validateCriteria(theCriteria, theFieldName); 127 SubscriptionMatchingStrategy strategy = mySubscriptionQueryValidator.determineStrategy(theCriteria); 128 if (strategy != SubscriptionMatchingStrategy.IN_MEMORY) { 129 ourLog.warn( 130 "Warning: Query Criteria '{}' in {} cannot be evaluated in-memory", theCriteria, theFieldName); 131 } 132 } catch (InvalidRequestException | DataFormatException e) { 133 throw new UnprocessableEntityException(Msg.code(2339) + "Invalid SubscriptionTopic criteria '" + theCriteria 134 + "' in " + theFieldName + ": " + e.getMessage()); 135 } 136 } 137}