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