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