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}