View Javadoc
1   package ca.uhn.fhir.jpa.dao.index;
2   
3   /*-
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2019 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.context.FhirContext;
24  import ca.uhn.fhir.context.RuntimeResourceDefinition;
25  import ca.uhn.fhir.context.RuntimeSearchParam;
26  import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
27  import ca.uhn.fhir.jpa.dao.DaoConfig;
28  import ca.uhn.fhir.jpa.dao.IDao;
29  import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
30  import ca.uhn.fhir.jpa.dao.MatchResourceUrlService;
31  import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
32  import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique;
33  import ca.uhn.fhir.jpa.model.entity.ResourceLink;
34  import ca.uhn.fhir.jpa.model.entity.ResourceTable;
35  import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
36  import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
37  import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor;
38  import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
39  import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
40  import ca.uhn.fhir.model.api.IQueryParameterType;
41  import ca.uhn.fhir.rest.api.server.RequestDetails;
42  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
43  import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
44  import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
45  import ca.uhn.fhir.util.FhirTerser;
46  import ca.uhn.fhir.util.UrlUtil;
47  import org.hl7.fhir.instance.model.api.IBaseReference;
48  import org.hl7.fhir.instance.model.api.IBaseResource;
49  import org.hl7.fhir.instance.model.api.IIdType;
50  import org.springframework.beans.factory.annotation.Autowired;
51  import org.springframework.context.annotation.Lazy;
52  import org.springframework.stereotype.Service;
53  
54  import javax.persistence.EntityManager;
55  import javax.persistence.PersistenceContext;
56  import javax.persistence.PersistenceContextType;
57  import java.util.*;
58  
59  import static org.apache.commons.lang3.StringUtils.isNotBlank;
60  
61  @Service
62  @Lazy
63  public class SearchParamWithInlineReferencesExtractor {
64  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class);
65  
66  	@Autowired
67  	private MatchResourceUrlService myMatchResourceUrlService;
68  	@Autowired
69  	private DaoConfig myDaoConfig;
70  	@Autowired
71  	private FhirContext myContext;
72  	@Autowired
73  	private IdHelperService myIdHelperService;
74  	@Autowired
75  	private ISearchParamRegistry mySearchParamRegistry;
76  	@Autowired
77  	SearchParamExtractorService mySearchParamExtractorService;
78  	@Autowired
79  	ResourceLinkExtractor myResourceLinkExtractor;
80  	@Autowired
81  	DaoResourceLinkResolver myDaoResourceLinkResolver;
82  	@Autowired
83  	DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
84  	@Autowired
85  	private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
86  
87  
88  	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
89  	protected EntityManager myEntityManager;
90  
91  	public void populateFromResource(ResourceIndexedSearchParams theParams, IDao theCallingDao, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams theExistingParams, RequestDetails theRequest) {
92  		mySearchParamExtractorService.extractFromResource(theParams, theEntity, theResource);
93  
94  		Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
95  		if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
96  			theParams.findMissingSearchParams(myDaoConfig.getModelConfig(), theEntity, activeSearchParams);
97  		}
98  
99  		theParams.setUpdatedTime(theUpdateTime);
100 
101 		extractInlineReferences(theResource, theRequest);
102 
103 		myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver, true, theRequest);
104 
105 		/*
106 		 * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
107 		 */
108 		for (Iterator<ResourceLink> existingLinkIter = theExistingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) {
109 			ResourceLink nextExisting = existingLinkIter.next();
110 			if (theParams.myLinks.remove(nextExisting)) {
111 				existingLinkIter.remove();
112 				theParams.myLinks.add(nextExisting);
113 			}
114 		}
115 
116 		/*
117 		 * Handle composites
118 		 */
119 		extractCompositeStringUniques(theEntity, theParams);
120 	}
121 
122 	private void extractCompositeStringUniques(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
123 
124 		final String resourceType = theEntity.getResourceType();
125 		List<JpaRuntimeSearchParam> uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(resourceType);
126 
127 		for (JpaRuntimeSearchParam next : uniqueSearchParams) {
128 
129 			List<List<String>> partsChoices = new ArrayList<>();
130 
131 			for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) {
132 				Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
133 				Collection<ResourceLink> linksForCompositePart = null;
134 				Collection<String> linksForCompositePartWantPaths = null;
135 				switch (nextCompositeOf.getParamType()) {
136 					case NUMBER:
137 						paramsListForCompositePart = theParams.myNumberParams;
138 						break;
139 					case DATE:
140 						paramsListForCompositePart = theParams.myDateParams;
141 						break;
142 					case STRING:
143 						paramsListForCompositePart = theParams.myStringParams;
144 						break;
145 					case TOKEN:
146 						paramsListForCompositePart = theParams.myTokenParams;
147 						break;
148 					case REFERENCE:
149 						linksForCompositePart = theParams.myLinks;
150 						linksForCompositePartWantPaths = new HashSet<>(nextCompositeOf.getPathsSplit());
151 						break;
152 					case QUANTITY:
153 						paramsListForCompositePart = theParams.myQuantityParams;
154 						break;
155 					case URI:
156 						paramsListForCompositePart = theParams.myUriParams;
157 						break;
158 					case SPECIAL:
159 					case COMPOSITE:
160 					case HAS:
161 						break;
162 				}
163 
164 				ArrayList<String> nextChoicesList = new ArrayList<>();
165 				partsChoices.add(nextChoicesList);
166 
167 				String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
168 				if (paramsListForCompositePart != null) {
169 					for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
170 						if (nextParam.getParamName().equals(nextCompositeOf.getName())) {
171 							IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
172 							String value = nextParamAsClientParam.getValueAsQueryToken(myContext);
173 							if (isNotBlank(value)) {
174 								value = UrlUtil.escapeUrlParam(value);
175 								nextChoicesList.add(key + "=" + value);
176 							}
177 						}
178 					}
179 				}
180 				if (linksForCompositePart != null) {
181 					for (ResourceLink nextLink : linksForCompositePart) {
182 						if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
183 							String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
184 							if (isNotBlank(value)) {
185 								value = UrlUtil.escapeUrlParam(value);
186 								nextChoicesList.add(key + "=" + value);
187 							}
188 						}
189 					}
190 				}
191 			}
192 
193 			Set<String> queryStringsToPopulate = ResourceIndexedSearchParams.extractCompositeStringUniquesValueChains(resourceType, partsChoices);
194 
195 			for (String nextQueryString : queryStringsToPopulate) {
196 				if (isNotBlank(nextQueryString)) {
197 					ourLog.trace("Adding composite unique SP: {}", nextQueryString);
198 					theParams.myCompositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
199 				}
200 			}
201 		}
202 	}
203 
204 
205 
206 	/**
207 	 * Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
208 	 * matching resource.
209 	 */
210 	public void extractInlineReferences(IBaseResource theResource, RequestDetails theRequest) {
211 		if (!myDaoConfig.isAllowInlineMatchUrlReferences()) {
212 			return;
213 		}
214 		FhirTerser terser = myContext.newTerser();
215 		List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
216 		for (IBaseReference nextRef : allRefs) {
217 			IIdType nextId = nextRef.getReferenceElement();
218 			String nextIdText = nextId.getValue();
219 			if (nextIdText == null) {
220 				continue;
221 			}
222 			int qmIndex = nextIdText.indexOf('?');
223 			if (qmIndex != -1) {
224 				for (int i = qmIndex - 1; i >= 0; i--) {
225 					if (nextIdText.charAt(i) == '/') {
226 						if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
227 							// Just in case the URL is in the form Patient/?foo=bar
228 							continue;
229 						}
230 						nextIdText = nextIdText.substring(i + 1);
231 						break;
232 					}
233 				}
234 				String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
235 				RuntimeResourceDefinition matchResourceDef = myContext.getResourceDefinition(resourceTypeString);
236 				if (matchResourceDef == null) {
237 					String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
238 					throw new InvalidRequestException(msg);
239 				}
240 				Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
241 				Set<Long> matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theRequest);
242 				if (matches.isEmpty()) {
243 					String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
244 					throw new ResourceNotFoundException(msg);
245 				}
246 				if (matches.size() > 1) {
247 					String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
248 					throw new PreconditionFailedException(msg);
249 				}
250 				Long next = matches.iterator().next();
251 				String newId = myIdHelperService.translatePidIdToForcedId(resourceTypeString, next);
252 				ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
253 				nextRef.setReference(newId);
254 			}
255 		}
256 	}
257 
258 	public void storeCompositeStringUniques(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
259 
260 		// Store composite string uniques
261 		if (myDaoConfig.isUniqueIndexesEnabled()) {
262 			for (ResourceIndexedCompositeStringUnique next : myDaoSearchParamSynchronizer.subtract(existingParams.myCompositeStringUniques, theParams.myCompositeStringUniques)) {
263 				ourLog.debug("Removing unique index: {}", next);
264 				myEntityManager.remove(next);
265 				theEntity.getParamsCompositeStringUnique().remove(next);
266 			}
267 			boolean haveNewParams = false;
268 			for (ResourceIndexedCompositeStringUnique next : myDaoSearchParamSynchronizer.subtract(theParams.myCompositeStringUniques, existingParams.myCompositeStringUniques)) {
269 				if (myDaoConfig.isUniqueIndexesCheckedBeforeSave()) {
270 					ResourceIndexedCompositeStringUnique existing = myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString());
271 					if (existing != null) {
272 						String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue());
273 						throw new PreconditionFailedException(msg);
274 					}
275 				}
276 				ourLog.debug("Persisting unique index: {}", next);
277 				myEntityManager.persist(next);
278 				haveNewParams = true;
279 			}
280 			if (theParams.myCompositeStringUniques.size() > 0 || haveNewParams) {
281 				theEntity.setParamsCompositeStringUniquePresent(true);
282 			} else {
283 				theEntity.setParamsCompositeStringUniquePresent(false);
284 			}
285 		}
286 	}
287 }