001/*
002 * #%L
003 * HAPI FHIR - Server Framework
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.rest.server;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
027import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
028import ca.uhn.fhir.rest.server.method.OperationMethodBinding;
029import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
030import ca.uhn.fhir.rest.server.method.SearchParameter;
031import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
032import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
033import ca.uhn.fhir.util.VersionUtil;
034import com.google.common.collect.ArrayListMultimap;
035import com.google.common.collect.ListMultimap;
036import jakarta.annotation.Nonnull;
037import jakarta.annotation.Nullable;
038import org.apache.commons.lang3.StringUtils;
039import org.apache.commons.lang3.Validate;
040import org.hl7.fhir.instance.model.api.IBaseResource;
041import org.hl7.fhir.instance.model.api.IIdType;
042import org.hl7.fhir.instance.model.api.IPrimitiveType;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046import java.util.ArrayList;
047import java.util.Collection;
048import java.util.Collections;
049import java.util.Comparator;
050import java.util.Date;
051import java.util.HashMap;
052import java.util.IdentityHashMap;
053import java.util.Iterator;
054import java.util.List;
055import java.util.Map;
056import java.util.Set;
057import java.util.TreeMap;
058import java.util.TreeSet;
059import java.util.stream.Collectors;
060
061import static org.apache.commons.lang3.StringUtils.isBlank;
062
063public class RestfulServerConfiguration implements ISearchParamRegistry {
064
065        private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerConfiguration.class);
066        private Collection<ResourceBinding> myResourceBindings;
067        private List<BaseMethodBinding> myServerBindings;
068        private List<BaseMethodBinding> myGlobalBindings;
069        private Map<String, Class<? extends IBaseResource>> myResourceNameToSharedSupertype;
070        private String myImplementationDescription;
071        private String myServerName = "HAPI FHIR";
072        private String myServerVersion = VersionUtil.getVersion();
073        private FhirContext myFhirContext;
074        private IServerAddressStrategy myServerAddressStrategy;
075        private IPrimitiveType<Date> myConformanceDate;
076
077        /**
078         * Constructor
079         */
080        public RestfulServerConfiguration() {
081                super();
082        }
083
084        /**
085         * Get the resourceBindings
086         *
087         * @return the resourceBindings
088         */
089        public Collection<ResourceBinding> getResourceBindings() {
090                return myResourceBindings;
091        }
092
093        /**
094         * Set the resourceBindings
095         *
096         * @param resourceBindings the resourceBindings to set
097         */
098        public RestfulServerConfiguration setResourceBindings(Collection<ResourceBinding> resourceBindings) {
099                this.myResourceBindings = resourceBindings;
100                return this;
101        }
102
103        /**
104         * Get the serverBindings
105         *
106         * @return the serverBindings
107         */
108        public List<BaseMethodBinding> getServerBindings() {
109                return myServerBindings;
110        }
111
112        /**
113         * Set the theServerBindings
114         */
115        public RestfulServerConfiguration setServerBindings(List<BaseMethodBinding> theServerBindings) {
116                this.myServerBindings = theServerBindings;
117                return this;
118        }
119
120        public Map<String, Class<? extends IBaseResource>> getNameToSharedSupertype() {
121                return myResourceNameToSharedSupertype;
122        }
123
124        public RestfulServerConfiguration setNameToSharedSupertype(
125                        Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype) {
126                this.myResourceNameToSharedSupertype = resourceNameToSharedSupertype;
127                return this;
128        }
129
130        /**
131         * Get the implementationDescription
132         *
133         * @return the implementationDescription
134         */
135        public String getImplementationDescription() {
136                if (isBlank(myImplementationDescription)) {
137                        return "HAPI FHIR";
138                }
139                return myImplementationDescription;
140        }
141
142        /**
143         * Set the implementationDescription
144         *
145         * @param implementationDescription the implementationDescription to set
146         */
147        public RestfulServerConfiguration setImplementationDescription(String implementationDescription) {
148                this.myImplementationDescription = implementationDescription;
149                return this;
150        }
151
152        /**
153         * Get the serverVersion
154         *
155         * @return the serverVersion
156         */
157        public String getServerVersion() {
158                return myServerVersion;
159        }
160
161        /**
162         * Set the serverVersion
163         *
164         * @param serverVersion the serverVersion to set
165         */
166        public RestfulServerConfiguration setServerVersion(String serverVersion) {
167                this.myServerVersion = serverVersion;
168                return this;
169        }
170
171        /**
172         * Get the serverName
173         *
174         * @return the serverName
175         */
176        public String getServerName() {
177                return myServerName;
178        }
179
180        /**
181         * Set the serverName
182         *
183         * @param serverName the serverName to set
184         */
185        public RestfulServerConfiguration setServerName(String serverName) {
186                this.myServerName = serverName;
187                return this;
188        }
189
190        /**
191         * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to
192         * creating their own.
193         */
194        public FhirContext getFhirContext() {
195                return this.myFhirContext;
196        }
197
198        /**
199         * Set the fhirContext
200         *
201         * @param fhirContext the fhirContext to set
202         */
203        public RestfulServerConfiguration setFhirContext(FhirContext fhirContext) {
204                this.myFhirContext = fhirContext;
205                return this;
206        }
207
208        /**
209         * Get the serverAddressStrategy
210         *
211         * @return the serverAddressStrategy
212         */
213        public IServerAddressStrategy getServerAddressStrategy() {
214                return myServerAddressStrategy;
215        }
216
217        /**
218         * Set the serverAddressStrategy
219         *
220         * @param serverAddressStrategy the serverAddressStrategy to set
221         */
222        public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) {
223                this.myServerAddressStrategy = serverAddressStrategy;
224        }
225
226        /**
227         * Get the date that will be specified in the conformance profile
228         * exported by this server. Typically this would be populated with
229         * an InstanceType.
230         */
231        public IPrimitiveType<Date> getConformanceDate() {
232                return myConformanceDate;
233        }
234
235        /**
236         * Set the date that will be specified in the conformance profile
237         * exported by this server. Typically this would be populated with
238         * an InstanceType.
239         */
240        public void setConformanceDate(IPrimitiveType<Date> theConformanceDate) {
241                myConformanceDate = theConformanceDate;
242        }
243
244        public Bindings provideBindings() {
245                IdentityHashMap<SearchMethodBinding, String> namedSearchMethodBindingToName = new IdentityHashMap<>();
246                HashMap<String, List<SearchMethodBinding>> searchNameToBindings = new HashMap<>();
247                IdentityHashMap<OperationMethodBinding, String> operationBindingToId = new IdentityHashMap<>();
248                HashMap<String, List<OperationMethodBinding>> operationIdToBindings = new HashMap<>();
249
250                Map<String, List<BaseMethodBinding>> resourceToMethods = collectMethodBindings();
251                List<BaseMethodBinding> methodBindings =
252                                resourceToMethods.values().stream().flatMap(t -> t.stream()).collect(Collectors.toList());
253                if (myGlobalBindings != null) {
254                        methodBindings.addAll(myGlobalBindings);
255                }
256
257                ListMultimap<String, OperationMethodBinding> nameToOperationMethodBindings = ArrayListMultimap.create();
258                for (BaseMethodBinding nextMethodBinding : methodBindings) {
259                        if (nextMethodBinding instanceof OperationMethodBinding) {
260                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
261                                nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding);
262                        } else if (nextMethodBinding instanceof SearchMethodBinding) {
263                                SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding;
264                                if (namedSearchMethodBindingToName.containsKey(methodBinding)) {
265                                        continue;
266                                }
267
268                                String name = createNamedQueryName(methodBinding);
269                                ourLog.trace("Detected named query: {}", name);
270
271                                namedSearchMethodBindingToName.put(methodBinding, name);
272                                if (!searchNameToBindings.containsKey(name)) {
273                                        searchNameToBindings.put(name, new ArrayList<>());
274                                }
275                                searchNameToBindings.get(name).add(methodBinding);
276                        }
277                }
278
279                for (String nextName : nameToOperationMethodBindings.keySet()) {
280                        List<OperationMethodBinding> nextMethodBindings = nameToOperationMethodBindings.get(nextName);
281
282                        boolean global = false;
283                        boolean system = false;
284                        boolean instance = false;
285                        boolean type = false;
286                        Set<String> resourceTypes = null;
287
288                        for (OperationMethodBinding nextMethodBinding : nextMethodBindings) {
289                                global |= nextMethodBinding.isGlobalMethod();
290                                system |= nextMethodBinding.isCanOperateAtServerLevel();
291                                type |= nextMethodBinding.isCanOperateAtTypeLevel();
292                                instance |= nextMethodBinding.isCanOperateAtInstanceLevel();
293                                if (nextMethodBinding.getResourceName() != null) {
294                                        resourceTypes = resourceTypes != null ? resourceTypes : new TreeSet<>();
295                                        resourceTypes.add(nextMethodBinding.getResourceName());
296                                }
297                        }
298
299                        StringBuilder operationIdBuilder = new StringBuilder();
300                        if (global) {
301                                operationIdBuilder.append("Global");
302                        } else if (resourceTypes != null && resourceTypes.size() == 1) {
303                                operationIdBuilder.append(resourceTypes.iterator().next());
304                        } else if (resourceTypes != null && resourceTypes.size() == 2) {
305                                Iterator<String> iterator = resourceTypes.iterator();
306                                operationIdBuilder.append(iterator.next());
307                                operationIdBuilder.append(iterator.next());
308                        } else if (resourceTypes != null) {
309                                operationIdBuilder.append("Multi");
310                        }
311
312                        operationIdBuilder.append('-');
313                        if (instance) {
314                                operationIdBuilder.append('i');
315                        }
316                        if (type) {
317                                operationIdBuilder.append('t');
318                        }
319                        if (system) {
320                                operationIdBuilder.append('s');
321                        }
322                        operationIdBuilder.append('-');
323
324                        // Exclude the leading $
325                        operationIdBuilder.append(nextName, 1, nextName.length());
326
327                        String operationId = operationIdBuilder.toString();
328                        operationIdToBindings.put(operationId, nextMethodBindings);
329                        nextMethodBindings.forEach(t -> operationBindingToId.put(t, operationId));
330                }
331
332                for (BaseMethodBinding nextMethodBinding : methodBindings) {
333                        if (nextMethodBinding instanceof OperationMethodBinding) {
334                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
335                                if (operationBindingToId.containsKey(methodBinding)) {
336                                        continue;
337                                }
338
339                                String name = createOperationName(methodBinding);
340                                ourLog.debug("Detected operation: {}", name);
341
342                                operationBindingToId.put(methodBinding, name);
343                                if (operationIdToBindings.containsKey(name) == false) {
344                                        operationIdToBindings.put(name, new ArrayList<>());
345                                }
346                                operationIdToBindings.get(name).add(methodBinding);
347                        }
348                }
349
350                return new Bindings(
351                                namedSearchMethodBindingToName, searchNameToBindings, operationIdToBindings, operationBindingToId);
352        }
353
354        public Map<String, List<BaseMethodBinding>> collectMethodBindings() {
355                Map<String, List<BaseMethodBinding>> resourceToMethods = new TreeMap<>();
356                for (ResourceBinding next : getResourceBindings()) {
357                        String resourceName = next.getResourceName();
358                        for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) {
359                                if (resourceToMethods.containsKey(resourceName) == false) {
360                                        resourceToMethods.put(resourceName, new ArrayList<>());
361                                }
362                                resourceToMethods.get(resourceName).add(nextMethodBinding);
363                        }
364                }
365                for (BaseMethodBinding nextMethodBinding : getServerBindings()) {
366                        String resourceName = "";
367                        if (resourceToMethods.containsKey(resourceName) == false) {
368                                resourceToMethods.put(resourceName, new ArrayList<>());
369                        }
370                        resourceToMethods.get(resourceName).add(nextMethodBinding);
371                }
372                return resourceToMethods;
373        }
374
375        public List<BaseMethodBinding> getGlobalBindings() {
376                return myGlobalBindings;
377        }
378
379        public void setGlobalBindings(List<BaseMethodBinding> theGlobalBindings) {
380                myGlobalBindings = theGlobalBindings;
381        }
382
383        /*
384         * Populates {@link #resourceNameToSharedSupertype} by scanning the given resource providers. Only resource provider getResourceType values
385         * are taken into account. {@link ProvidesResources} and method return types are deliberately ignored.
386         *
387         * Given a resource name, the common superclass for all getResourceType return values for that name's providers is the common superclass
388         * for all returned/received resources with that name. Since {@link ProvidesResources} resources and method return types must also be
389         * subclasses of this common supertype, they can't affect the result of this method.
390         */
391        public void computeSharedSupertypeForResourcePerName(Collection<IResourceProvider> providers) {
392                Map<String, CommonResourceSupertypeScanner> resourceNameToScanner = new HashMap<>();
393
394                List<Class<? extends IBaseResource>> providedResourceClasses =
395                                providers.stream().map(provider -> provider.getResourceType()).collect(Collectors.toList());
396                providedResourceClasses.stream().forEach(resourceClass -> {
397                        RuntimeResourceDefinition baseDefinition =
398                                        getFhirContext().getResourceDefinition(resourceClass).getBaseDefinition();
399                        CommonResourceSupertypeScanner scanner = resourceNameToScanner.computeIfAbsent(
400                                        baseDefinition.getName(), key -> new CommonResourceSupertypeScanner());
401                        scanner.register(resourceClass);
402                });
403
404                myResourceNameToSharedSupertype = resourceNameToScanner.entrySet().stream()
405                                .filter(entry -> entry.getValue().getLowestCommonSuperclass().isPresent())
406                                .collect(Collectors.toMap(
407                                                entry -> entry.getKey(),
408                                                entry -> entry.getValue().getLowestCommonSuperclass().get()));
409        }
410
411        private String createNamedQueryName(SearchMethodBinding searchMethodBinding) {
412                StringBuilder retVal = new StringBuilder();
413                if (searchMethodBinding.getResourceName() != null) {
414                        retVal.append(searchMethodBinding.getResourceName());
415                }
416                retVal.append("-query-");
417                retVal.append(searchMethodBinding.getQueryName());
418
419                return retVal.toString();
420        }
421
422        @Override
423        public RuntimeSearchParam getActiveSearchParam(
424                        @Nonnull String theResourceName,
425                        @Nonnull String theParamName,
426                        @Nonnull SearchParamLookupContextEnum theContext) {
427                return getActiveSearchParams(theResourceName, theContext).get(theParamName);
428        }
429
430        @Override
431        public ResourceSearchParams getActiveSearchParams(
432                        @Nonnull String theResourceName, @Nonnull SearchParamLookupContextEnum theContext) {
433                Validate.notBlank(theResourceName, "theResourceName must not be null or blank");
434
435                ResourceSearchParams retval = new ResourceSearchParams(theResourceName);
436
437                collectMethodBindings().getOrDefault(theResourceName, Collections.emptyList()).stream()
438                                .filter(t -> theResourceName.equals(t.getResourceName()))
439                                .filter(t -> t instanceof SearchMethodBinding)
440                                .map(t -> (SearchMethodBinding) t)
441                                .filter(t -> t.getQueryName() == null)
442                                .forEach(t -> createRuntimeBinding(retval, t));
443
444                return retval;
445        }
446
447        @Nullable
448        @Override
449        public RuntimeSearchParam getActiveSearchParamByUrl(
450                        @Nonnull String theUrl, @Nonnull SearchParamLookupContextEnum theContext) {
451                throw new UnsupportedOperationException(Msg.code(286));
452        }
453
454        private void createRuntimeBinding(
455                        ResourceSearchParams theMapToPopulate, SearchMethodBinding theSearchMethodBinding) {
456
457                List<SearchParameter> parameters = theSearchMethodBinding.getParameters().stream()
458                                .filter(t -> t instanceof SearchParameter)
459                                .map(t -> (SearchParameter) t)
460                                .sorted(SearchParameterComparator.INSTANCE)
461                                .collect(Collectors.toList());
462
463                for (SearchParameter nextParameter : parameters) {
464
465                        String nextParamName = nextParameter.getName();
466
467                        String nextParamUnchainedName = nextParamName;
468                        if (nextParamName.contains(".")) {
469                                nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
470                        }
471
472                        String nextParamDescription = nextParameter.getDescription();
473
474                        /*
475                         * If the parameter has no description, default to the one from the resource
476                         */
477                        if (StringUtils.isBlank(nextParamDescription)) {
478                                RuntimeResourceDefinition def =
479                                                getFhirContext().getResourceDefinition(theSearchMethodBinding.getResourceName());
480                                RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
481                                if (paramDef != null) {
482                                        nextParamDescription = paramDef.getDescription();
483                                }
484                        }
485
486                        if (theMapToPopulate.containsParamName(nextParamUnchainedName)) {
487                                continue;
488                        }
489
490                        IIdType id = getFhirContext()
491                                        .getVersion()
492                                        .newIdType()
493                                        .setValue("SearchParameter/" + theSearchMethodBinding.getResourceName() + "-" + nextParamName);
494                        String uri = null;
495                        String description = nextParamDescription;
496                        String path = null;
497                        RestSearchParameterTypeEnum type = nextParameter.getParamType();
498                        Set<String> providesMembershipInCompartments = Collections.emptySet();
499                        Set<String> targets = Collections.emptySet();
500                        RuntimeSearchParam.RuntimeSearchParamStatusEnum status =
501                                        RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
502                        Collection<String> base = Collections.singletonList(theSearchMethodBinding.getResourceName());
503                        RuntimeSearchParam param = new RuntimeSearchParam(
504                                        id,
505                                        uri,
506                                        nextParamName,
507                                        description,
508                                        path,
509                                        type,
510                                        providesMembershipInCompartments,
511                                        targets,
512                                        status,
513                                        null,
514                                        null,
515                                        base);
516                        theMapToPopulate.put(nextParamName, param);
517                }
518        }
519
520        private static class SearchParameterComparator implements Comparator<SearchParameter> {
521                private static final SearchParameterComparator INSTANCE = new SearchParameterComparator();
522
523                @Override
524                public int compare(SearchParameter theO1, SearchParameter theO2) {
525                        if (theO1.isRequired() == theO2.isRequired()) {
526                                return theO1.getName().compareTo(theO2.getName());
527                        }
528                        if (theO1.isRequired()) {
529                                return -1;
530                        }
531                        return 1;
532                }
533        }
534
535        private static String createOperationName(OperationMethodBinding theMethodBinding) {
536                StringBuilder retVal = new StringBuilder();
537                if (theMethodBinding.getResourceName() != null) {
538                        retVal.append(theMethodBinding.getResourceName());
539                } else if (theMethodBinding.isGlobalMethod()) {
540                        retVal.append("Global");
541                }
542
543                retVal.append('-');
544                if (theMethodBinding.isCanOperateAtInstanceLevel()) {
545                        retVal.append('i');
546                }
547                if (theMethodBinding.isCanOperateAtServerLevel()) {
548                        retVal.append('s');
549                }
550                retVal.append('-');
551
552                // Exclude the leading $
553                retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
554
555                return retVal.toString();
556        }
557}