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}