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}