
001/*- 002 * #%L 003 * HAPI FHIR JPA 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.dao.search; 021 022import ca.uhn.fhir.context.RuntimeSearchParam; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 025import ca.uhn.fhir.rest.api.Constants; 026import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 027import ca.uhn.fhir.rest.api.SortOrderEnum; 028import ca.uhn.fhir.rest.api.SortSpec; 029import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 030import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 031import com.google.common.annotations.VisibleForTesting; 032import org.hibernate.search.engine.search.sort.dsl.SearchSortFactory; 033import org.hibernate.search.engine.search.sort.dsl.SortFinalStep; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import java.util.List; 038import java.util.Map; 039import java.util.Optional; 040import java.util.stream.Collectors; 041 042import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.IDX_STRING_LOWER; 043import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.INDEX_TYPE_QUANTITY; 044import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT; 045import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NUMBER_VALUE; 046import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE; 047import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE_NORM; 048import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.SEARCH_PARAM_ROOT; 049import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.URI_VALUE; 050import static java.util.Objects.isNull; 051 052/** 053 * Used to build HSearch sort clauses. 054 */ 055public class HSearchSortHelperImpl implements IHSearchSortHelper { 056 private static final Logger ourLog = LoggerFactory.getLogger(HSearchSortHelperImpl.class); 057 public static final Map<String, RestSearchParameterTypeEnum> ourSortingParamNameToParamType = Map.of( 058 Constants.PARAM_LASTUPDATED, RestSearchParameterTypeEnum.DATE, 059 Constants.PARAM_ID, RestSearchParameterTypeEnum.TOKEN, 060 Constants.PARAM_TAG, RestSearchParameterTypeEnum.TOKEN, 061 Constants.PARAM_SECURITY, RestSearchParameterTypeEnum.TOKEN, 062 Constants.PARAM_SOURCE, RestSearchParameterTypeEnum.TOKEN); 063 064 /** Indicates which HSearch properties must be sorted for each RestSearchParameterTypeEnum **/ 065 private Map<RestSearchParameterTypeEnum, List<String>> mySortPropertyListMap = Map.of( 066 RestSearchParameterTypeEnum.STRING, List.of(SEARCH_PARAM_ROOT + ".*.string." + IDX_STRING_LOWER), 067 RestSearchParameterTypeEnum.TOKEN, 068 List.of( 069 String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", "token", "system"), 070 String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", "token", "code")), 071 RestSearchParameterTypeEnum.REFERENCE, List.of(SEARCH_PARAM_ROOT + ".*.reference.value"), 072 RestSearchParameterTypeEnum.DATE, List.of(SEARCH_PARAM_ROOT + ".*.dt.lower"), 073 RestSearchParameterTypeEnum.QUANTITY, 074 List.of( 075 String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", INDEX_TYPE_QUANTITY, QTY_VALUE_NORM), 076 String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", INDEX_TYPE_QUANTITY, QTY_VALUE)), 077 RestSearchParameterTypeEnum.URI, List.of(SEARCH_PARAM_ROOT + ".*." + URI_VALUE), 078 RestSearchParameterTypeEnum.NUMBER, List.of(SEARCH_PARAM_ROOT + ".*." + NUMBER_VALUE)); 079 080 private final ISearchParamRegistry mySearchParamRegistry; 081 082 public HSearchSortHelperImpl(ISearchParamRegistry theSearchParamRegistry) { 083 mySearchParamRegistry = theSearchParamRegistry; 084 } 085 086 /** 087 * Builds and returns sort clauses for received sort parameters 088 */ 089 @Override 090 public SortFinalStep getSortClauses( 091 SearchSortFactory theSortFactory, SortSpec theSortParams, String theResourceType) { 092 var sortStep = theSortFactory.composite(); 093 Optional<SortFinalStep> sortClauseOpt = getSortClause(theSortFactory, theSortParams, theResourceType); 094 sortClauseOpt.ifPresent(sortStep::add); 095 096 SortSpec nextParam = theSortParams.getChain(); 097 while (nextParam != null) { 098 sortClauseOpt = getSortClause(theSortFactory, nextParam, theResourceType); 099 sortClauseOpt.ifPresent(sortStep::add); 100 101 nextParam = nextParam.getChain(); 102 } 103 104 return sortStep; 105 } 106 107 @Override 108 public boolean supportsAllSortTerms(String theResourceType, SearchParameterMap theParams) { 109 for (SortSpec sortSpec : theParams.getAllChainsInOrder()) { 110 final Optional<RestSearchParameterTypeEnum> paramTypeOpt = 111 getParamType(theResourceType, sortSpec.getParamName()); 112 if (paramTypeOpt.isEmpty()) { 113 return false; 114 } 115 } 116 117 return true; 118 } 119 120 /** 121 * Builds sort clauses for the received SortSpec by 122 * _ finding out the corresponding RestSearchParameterTypeEnum for the parameter 123 * _ obtaining the list of properties to sort for the found parameter type 124 * _ building the sort clauses for the found list of properties 125 */ 126 @VisibleForTesting 127 Optional<SortFinalStep> getSortClause(SearchSortFactory theF, SortSpec theSortSpec, String theResourceType) { 128 Optional<RestSearchParameterTypeEnum> paramTypeOpt = getParamType(theResourceType, theSortSpec.getParamName()); 129 if (paramTypeOpt.isEmpty()) { 130 throw new IllegalArgumentException( 131 Msg.code(2523) + "Invalid sort specification: " + theSortSpec.getParamName()); 132 } 133 List<String> paramFieldNameList = getSortPropertyList(paramTypeOpt.get(), theSortSpec.getParamName()); 134 if (paramFieldNameList.isEmpty()) { 135 ourLog.warn("Unable to sort by parameter '{}' . Sort parameter ignored.", theSortSpec.getParamName()); 136 return Optional.empty(); 137 } 138 139 var sortFinalStep = theF.composite(); 140 for (String fieldName : paramFieldNameList) { 141 var sortStep = theF.field(fieldName); 142 143 if (theSortSpec.getOrder().equals(SortOrderEnum.DESC)) { 144 sortStep.desc(); 145 } else { 146 sortStep.asc(); 147 } 148 149 // field could have no value 150 sortFinalStep.add(sortStep.missing().last()); 151 } 152 153 // regular sorting is supported 154 return Optional.of(sortFinalStep); 155 } 156 157 /** 158 * Finds out and returns the parameter type for each parameter name 159 */ 160 @VisibleForTesting 161 Optional<RestSearchParameterTypeEnum> getParamType(String theResourceTypeName, String theParamName) { 162 RestSearchParameterTypeEnum value; 163 164 ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams( 165 theResourceTypeName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 166 RuntimeSearchParam searchParam = activeSearchParams.get(theParamName); 167 168 if (searchParam == null) { 169 value = isNull(theParamName) ? null : ourSortingParamNameToParamType.get(theParamName); 170 171 } else { 172 value = searchParam.getParamType(); 173 } 174 175 return Optional.ofNullable(value); 176 } 177 178 /** 179 * Retrieves the generic property names (* instead of parameter name) from the configured map and 180 * replaces the '*' segment by theParamName before returning the final property name list 181 */ 182 @VisibleForTesting 183 List<String> getSortPropertyList(RestSearchParameterTypeEnum theParamType, String theParamName) { 184 List<String> paramFieldNameList = mySortPropertyListMap.get(theParamType); 185 // replace '*' names segment by theParamName 186 return paramFieldNameList.stream() 187 .map(s -> s.replace("*", theParamName)) 188 .collect(Collectors.toList()); 189 } 190}