001/*-
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2026 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.repository.impl;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.model.api.IQueryParameterType;
024import ca.uhn.fhir.repository.IRepository.IRepositoryRestQueryContributor;
025import ca.uhn.fhir.repository.IRepositoryRestQueryBuilder;
026import ca.uhn.fhir.rest.param.ParameterUtil;
027import com.google.common.collect.ArrayListMultimap;
028import com.google.common.collect.Lists;
029import com.google.common.collect.Multimap;
030import jakarta.annotation.Nonnull;
031
032import java.util.Collection;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036
037import static java.util.Objects.requireNonNull;
038
039/**
040 * This class provides a rest-query builder over a plain Multimap.
041 * It is used to convert {@link IRepositoryRestQueryContributor} implementations
042 * that are not Multimap-based so they can be used by IRepository implementations that are.
043 */
044public class MultiMapRepositoryRestQueryBuilder implements IRepositoryRestQueryBuilder {
045        /**
046         * Our search parameters.
047         * We use a list multimap to maintain insertion order, and because most of IQueryParameterType don't
048         * provide a meaningful equals/hashCode implementation.
049         */
050        private final Multimap<String, List<IQueryParameterType>> mySearchParameters = ArrayListMultimap.create();
051
052        @Nonnull
053        public static Map<String, String[]> toFlatMap(IRepositoryRestQueryContributor searchParameterMap) {
054                MultiMapRepositoryRestQueryBuilder builder = new MultiMapRepositoryRestQueryBuilder();
055                searchParameterMap.contributeToQuery(builder);
056                Multimap<String, List<IQueryParameterType>> m = builder.toMultiMap();
057                return flattenMultimap(m);
058        }
059
060        /**
061         * Converts a Multimap of search parameters to a flat Map of Strings.
062         */
063        @Nonnull
064        static Map<String, String[]> flattenMultimap(Multimap<String, List<IQueryParameterType>> theQueryMap) {
065                Map<String, String[]> result = new HashMap<>();
066
067                theQueryMap.asMap().forEach((key, value) -> result.put(key, flattenValues(value)));
068
069                return result;
070        }
071
072        /**
073         * Flatten the and/or lists of IQueryParameterType into a String array.
074         */
075        private static @Nonnull String[] flattenValues(Collection<List<IQueryParameterType>> theOrLists) {
076                // hacky - this ignores modifiers.  Those should probably move back to the keys for this legacy api.
077                // But this is a dead api.
078                return theOrLists.stream()
079                                .map(MultiMapRepositoryRestQueryBuilder::flattenOrList)
080                                .toArray(String[]::new);
081        }
082
083        /** Build an or-list string */
084        private static String flattenOrList(List<IQueryParameterType> theOrList) {
085                return ParameterUtil.escapeAndJoinOrList(
086                                Lists.transform(theOrList, p -> requireNonNull(p).getValueAsQueryToken()));
087        }
088
089        @Override
090        public IRepositoryRestQueryBuilder addOrList(String theParamName, List<IQueryParameterType> theParameters) {
091                validateHomogeneousList(theParamName, theParameters);
092                mySearchParameters.put(theParamName, theParameters);
093                return this;
094        }
095
096        private void validateHomogeneousList(String theName, List<IQueryParameterType> theValues) {
097                if (theValues.isEmpty()) {
098                        return;
099                }
100                IQueryParameterType firstValue = theValues.get(0);
101                for (IQueryParameterType nextValue : theValues) {
102                        if (!nextValue.getClass().equals(firstValue.getClass())) {
103                                throw new IllegalArgumentException(
104                                                Msg.code(2833) + "All parameters in an or-list must be of the same type. Found "
105                                                                + firstValue.getClass().getSimpleName() + " and "
106                                                                + nextValue.getClass().getSimpleName() + " in parameter '" + theName + "'");
107                        }
108                }
109        }
110
111        public Multimap<String, List<IQueryParameterType>> toMultiMap() {
112                return mySearchParameters;
113        }
114
115        /**
116         * Converts a {@link IRepositoryRestQueryContributor} to a Multimap.
117         *
118         * @param theSearchQueryBuilder the contributor to convert
119         * @return a Multimap containing the search parameters contributed by the contributor
120         */
121        public static Multimap<String, List<IQueryParameterType>> contributorToMultimap(
122                        IRepositoryRestQueryContributor theSearchQueryBuilder) {
123                MultiMapRepositoryRestQueryBuilder builder = new MultiMapRepositoryRestQueryBuilder();
124                theSearchQueryBuilder.contributeToQuery(builder);
125                return builder.toMultiMap();
126        }
127}