
001package ca.uhn.fhir.jpa.dao.search; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 025import ca.uhn.fhir.model.api.IQueryParameterType; 026import ca.uhn.fhir.rest.api.Constants; 027import ca.uhn.fhir.rest.param.DateParam; 028import ca.uhn.fhir.rest.param.NumberParam; 029import ca.uhn.fhir.rest.param.QuantityParam; 030import ca.uhn.fhir.rest.param.ReferenceParam; 031import ca.uhn.fhir.rest.param.StringParam; 032import ca.uhn.fhir.rest.param.TokenParam; 033import ca.uhn.fhir.rest.param.UriParam; 034import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 035import com.google.common.collect.Lists; 036import com.google.common.collect.Sets; 037import org.apache.commons.lang3.StringUtils; 038 039import java.util.ArrayList; 040import java.util.Collection; 041import java.util.List; 042import java.util.Set; 043 044/** 045 * Search builder for lucene/elastic for token, string, and reference parameters. 046 */ 047public class ExtendedLuceneSearchBuilder { 048 public static final String EMPTY_MODIFIER = ""; 049 050 /** 051 * These params have complicated semantics, or are best resolved at the JPA layer for now. 052 */ 053 public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_meta"); 054 055 /** 056 * Are any of the queries supported by our indexing? 057 */ 058 public boolean isSupportsSomeOf(SearchParameterMap myParams) { 059 return 060 myParams.getSort() != null || 061 myParams.getLastUpdated() != null || 062 myParams.entrySet().stream() 063 .filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey())) 064 // each and clause may have a different modifier, so split down to the ORs 065 .flatMap(andList -> andList.getValue().stream()) 066 .flatMap(Collection::stream) 067 .anyMatch(this::isParamTypeSupported); 068 } 069 070 /** 071 * Do we support this query param type+modifier? 072 * <p> 073 * NOTE - keep this in sync with addAndConsumeAdvancedQueryClauses() below. 074 */ 075 private boolean isParamTypeSupported(IQueryParameterType param) { 076 String modifier = StringUtils.defaultString(param.getQueryParameterQualifier(), EMPTY_MODIFIER); 077 if (param instanceof TokenParam) { 078 switch (modifier) { 079 case Constants.PARAMQUALIFIER_TOKEN_TEXT: 080 case "": 081 // we support plain token and token:text 082 return true; 083 default: 084 return false; 085 } 086 } else if (param instanceof StringParam) { 087 switch (modifier) { 088 // we support string:text, string:contains, string:exact, and unmodified string. 089 case Constants.PARAMQUALIFIER_TOKEN_TEXT: 090 case Constants.PARAMQUALIFIER_STRING_EXACT: 091 case Constants.PARAMQUALIFIER_STRING_CONTAINS: 092 case EMPTY_MODIFIER: 093 return true; 094 default: 095 return false; 096 } 097 } else if (param instanceof QuantityParam) { 098 return modifier.equals(EMPTY_MODIFIER); 099 100 } else if (param instanceof ReferenceParam) { 101 //We cannot search by chain. 102 if (((ReferenceParam) param).getChain() != null) { 103 return false; 104 } 105 switch (modifier) { 106 case EMPTY_MODIFIER: 107 return true; 108 case Constants.PARAMQUALIFIER_MDM: 109 case Constants.PARAMQUALIFIER_NICKNAME: 110 default: 111 return false; 112 } 113 } else if (param instanceof DateParam) { 114 if (EMPTY_MODIFIER.equals(modifier)) { 115 return true; 116 } 117 return false; 118 } else if (param instanceof UriParam) { 119 return modifier.equals(EMPTY_MODIFIER); 120 121 } else if (param instanceof NumberParam) { 122 return modifier.equals(EMPTY_MODIFIER); 123 124 } else { 125 return false; 126 } 127 } 128 129 public void addAndConsumeAdvancedQueryClauses(ExtendedLuceneClauseBuilder builder, String theResourceType, SearchParameterMap theParams, ISearchParamRegistry theSearchParamRegistry) { 130 // copy the keys to avoid concurrent modification error 131 ArrayList<String> paramNames = compileParamNames(theParams); 132 for (String nextParam : paramNames) { 133 if (ourUnsafeSearchParmeters.contains(nextParam)) { 134 continue; 135 } 136 RuntimeSearchParam activeParam = theSearchParamRegistry.getActiveSearchParam(theResourceType, nextParam); 137 if (activeParam == null) { 138 // ignore magic params handled in JPA 139 continue; 140 } 141 142 // NOTE - keep this in sync with isParamSupported() above. 143 switch (activeParam.getParamType()) { 144 case TOKEN: 145 List<List<IQueryParameterType>> tokenTextAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT); 146 builder.addStringTextSearch(nextParam, tokenTextAndOrTerms); 147 148 List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam); 149 builder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms); 150 break; 151 152 case STRING: 153 List<List<IQueryParameterType>> stringTextAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT); 154 builder.addStringTextSearch(nextParam, stringTextAndOrTerms); 155 156 List<List<IQueryParameterType>> stringExactAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_EXACT); 157 builder.addStringExactSearch(nextParam, stringExactAndOrTerms); 158 159 List<List<IQueryParameterType>> stringContainsAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS); 160 builder.addStringContainsSearch(nextParam, stringContainsAndOrTerms); 161 162 List<List<IQueryParameterType>> stringAndOrTerms = theParams.removeByNameUnmodified(nextParam); 163 builder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms); 164 break; 165 166 case QUANTITY: 167 List<List<IQueryParameterType>> quantityAndOrTerms = theParams.removeByNameUnmodified(nextParam); 168 builder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms); 169 break; 170 171 case REFERENCE: 172 List<List<IQueryParameterType>> referenceAndOrTerms = theParams.removeByNameUnmodified(nextParam); 173 builder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms); 174 break; 175 176 case DATE: 177 List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated") 178 ? getLastUpdatedAndOrList(theParams) : theParams.removeByNameUnmodified(nextParam); 179 builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms); 180 break; 181 182 case URI: 183 List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam); 184 builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms); 185 break; 186 187 case NUMBER: 188 List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = theParams.remove(nextParam); 189 builder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms); 190 break; 191 192 default: 193 // ignore unsupported param types/modifiers. They will be processed up in SearchBuilder. 194 } 195 } 196 } 197 198 199 private List<List<IQueryParameterType>> getLastUpdatedAndOrList(SearchParameterMap theParams) { 200 DateParam activeBound = theParams.getLastUpdated().getLowerBound() != null 201 ? theParams.getLastUpdated().getLowerBound() 202 : theParams.getLastUpdated().getUpperBound(); 203 204 List<List<IQueryParameterType>> result = List.of( List.of(activeBound) ); 205 206 // indicate parameter was processed 207 theParams.setLastUpdated(null); 208 209 return result; 210 } 211 212 213 /** 214 * Param name list is not only the params.keySet, but also the "special" parameters extracted from input 215 * (as _lastUpdated when the input myLastUpdated field is not null, etc). 216 */ 217 private ArrayList<String> compileParamNames(SearchParameterMap theParams) { 218 ArrayList<String> nameList = Lists.newArrayList(theParams.keySet()); 219 220 if (theParams.getLastUpdated() != null) { 221 nameList.add("_lastUpdated"); 222 } 223 224 return nameList; 225 } 226 227}