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