001/*- 002 * #%L 003 * HAPI FHIR JPA 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.dao.index; 021 022import ca.uhn.fhir.context.RuntimeSearchParam; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; 026import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboStringUniqueDao; 027import ca.uhn.fhir.jpa.model.config.PartitionSettings; 028import ca.uhn.fhir.jpa.model.dao.JpaPid; 029import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; 030import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; 031import ca.uhn.fhir.jpa.model.entity.ResourceTable; 032import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamWithInlineReferencesExtractor; 033import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; 034import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamWithInlineReferencesExtractor; 035import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 036import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; 037import ca.uhn.fhir.rest.api.server.RequestDetails; 038import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 039import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 040import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 041import com.google.common.annotations.VisibleForTesting; 042import jakarta.annotation.Nullable; 043import jakarta.persistence.EntityManager; 044import jakarta.persistence.PersistenceContext; 045import jakarta.persistence.PersistenceContextType; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047import org.springframework.beans.factory.annotation.Autowired; 048import org.springframework.context.annotation.Lazy; 049import org.springframework.stereotype.Service; 050 051import java.util.Collection; 052import java.util.stream.Collectors; 053 054@Service 055@Lazy 056public class SearchParamWithInlineReferencesExtractor extends BaseSearchParamWithInlineReferencesExtractor<JpaPid> 057 implements ISearchParamWithInlineReferencesExtractor { 058 private static final org.slf4j.Logger ourLog = 059 org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class); 060 061 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 062 protected EntityManager myEntityManager; 063 064 @Autowired 065 private ISearchParamRegistry mySearchParamRegistry; 066 067 @Autowired 068 private SearchParamExtractorService mySearchParamExtractorService; 069 070 @Autowired 071 private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer; 072 073 @Autowired 074 private IResourceIndexedComboStringUniqueDao myResourceIndexedCompositeStringUniqueDao; 075 076 @Autowired 077 private PartitionSettings myPartitionSettings; 078 079 @VisibleForTesting 080 public void setPartitionSettings(PartitionSettings thePartitionSettings) { 081 myPartitionSettings = thePartitionSettings; 082 } 083 084 @VisibleForTesting 085 public void setSearchParamExtractorService(SearchParamExtractorService theSearchParamExtractorService) { 086 mySearchParamExtractorService = theSearchParamExtractorService; 087 } 088 089 @VisibleForTesting 090 public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { 091 mySearchParamRegistry = theSearchParamRegistry; 092 } 093 094 public void populateFromResource( 095 RequestPartitionId theRequestPartitionId, 096 ResourceIndexedSearchParams theParams, 097 TransactionDetails theTransactionDetails, 098 ResourceTable theEntity, 099 IBaseResource theResource, 100 ResourceIndexedSearchParams theExistingParams, 101 RequestDetails theRequest, 102 boolean thePerformIndexing) { 103 if (thePerformIndexing) { 104 // Perform inline match URL substitution 105 extractInlineReferences(theRequest, theResource, theTransactionDetails); 106 } 107 108 mySearchParamExtractorService.extractFromResource( 109 theRequestPartitionId, 110 theRequest, 111 theParams, 112 theExistingParams, 113 theEntity, 114 theResource, 115 theTransactionDetails, 116 thePerformIndexing, 117 ISearchParamExtractor.ALL_PARAMS); 118 } 119 120 @Nullable 121 private Collection<? extends BaseResourceIndexedSearchParam> findParameterIndexes( 122 ResourceIndexedSearchParams theParams, RuntimeSearchParam nextCompositeOf) { 123 Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null; 124 switch (nextCompositeOf.getParamType()) { 125 case NUMBER: 126 paramsListForCompositePart = theParams.myNumberParams; 127 break; 128 case DATE: 129 paramsListForCompositePart = theParams.myDateParams; 130 break; 131 case STRING: 132 paramsListForCompositePart = theParams.myStringParams; 133 break; 134 case TOKEN: 135 paramsListForCompositePart = theParams.myTokenParams; 136 break; 137 case QUANTITY: 138 paramsListForCompositePart = theParams.myQuantityParams; 139 break; 140 case URI: 141 paramsListForCompositePart = theParams.myUriParams; 142 break; 143 case REFERENCE: 144 case SPECIAL: 145 case COMPOSITE: 146 case HAS: 147 break; 148 } 149 if (paramsListForCompositePart != null) { 150 paramsListForCompositePart = paramsListForCompositePart.stream() 151 .filter(t -> t.getParamName().equals(nextCompositeOf.getName())) 152 .collect(Collectors.toList()); 153 } 154 return paramsListForCompositePart; 155 } 156 157 @VisibleForTesting 158 public void setDaoSearchParamSynchronizer(DaoSearchParamSynchronizer theDaoSearchParamSynchronizer) { 159 myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer; 160 } 161 162 public void storeUniqueComboParameters( 163 ResourceIndexedSearchParams theParams, 164 ResourceTable theEntity, 165 ResourceIndexedSearchParams theExistingParams) { 166 167 /* 168 * String Uniques 169 */ 170 if (myStorageSettings.isUniqueIndexesEnabled()) { 171 for (ResourceIndexedComboStringUnique next : DaoSearchParamSynchronizer.subtract( 172 theExistingParams.myComboStringUniques, theParams.myComboStringUniques)) { 173 ourLog.debug("Removing unique index: {}", next); 174 myEntityManager.remove(next); 175 theEntity.getParamsComboStringUnique().remove(next); 176 } 177 boolean haveNewStringUniqueParams = false; 178 for (ResourceIndexedComboStringUnique next : DaoSearchParamSynchronizer.subtract( 179 theParams.myComboStringUniques, theExistingParams.myComboStringUniques)) { 180 if (myStorageSettings.isUniqueIndexesCheckedBeforeSave()) { 181 ResourceIndexedComboStringUnique existing = 182 myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString()); 183 if (existing != null) { 184 185 String searchParameterId = "(unknown)"; 186 if (next.getSearchParameterId() != null) { 187 searchParameterId = next.getSearchParameterId() 188 .toUnqualifiedVersionless() 189 .getValue(); 190 } 191 192 String msg = myFhirContext 193 .getLocalizer() 194 .getMessage( 195 BaseHapiFhirDao.class, 196 "uniqueIndexConflictFailure", 197 theEntity.getResourceType(), 198 next.getIndexString(), 199 existing.getResource() 200 .getIdDt() 201 .toUnqualifiedVersionless() 202 .getValue(), 203 searchParameterId); 204 205 // Use ResourceVersionConflictException here because the HapiTransactionService 206 // catches this and can retry it if needed 207 throw new ResourceVersionConflictException(Msg.code(1093) + msg); 208 } 209 } 210 ourLog.debug("Persisting unique index: {}", next); 211 myEntityManager.persist(next); 212 haveNewStringUniqueParams = true; 213 } 214 theEntity.setParamsComboStringUniquePresent( 215 theParams.myComboStringUniques.size() > 0 || haveNewStringUniqueParams); 216 } 217 } 218}