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.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.context.RuntimeSearchParam;
026import ca.uhn.fhir.i18n.Msg;
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.rest.server.util.ResourceSearchParams;
034import ca.uhn.fhir.util.VersionUtil;
035import com.google.common.collect.ArrayListMultimap;
036import com.google.common.collect.ListMultimap;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.commons.lang3.Validate;
039import org.hl7.fhir.instance.model.api.IBaseResource;
040import org.hl7.fhir.instance.model.api.IIdType;
041import org.hl7.fhir.instance.model.api.IPrimitiveType;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import javax.annotation.Nonnull;
046import javax.annotation.Nullable;
047import java.util.ArrayList;
048import java.util.Collection;
049import java.util.Collections;
050import java.util.Comparator;
051import java.util.Date;
052import java.util.HashMap;
053import java.util.IdentityHashMap;
054import java.util.Iterator;
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        private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerConfiguration.class);
067        private Collection<ResourceBinding> myResourceBindings;
068        private List<BaseMethodBinding> myServerBindings;
069        private List<BaseMethodBinding> myGlobalBindings;
070        private Map<String, Class<? extends IBaseResource>> myResourceNameToSharedSupertype;
071        private String myImplementationDescription;
072        private String myServerName = "HAPI FHIR";
073        private String myServerVersion = VersionUtil.getVersion();
074        private FhirContext myFhirContext;
075        private IServerAddressStrategy myServerAddressStrategy;
076        private IPrimitiveType<Date> myConformanceDate;
077
078        /**
079         * Constructor
080         */
081        public RestfulServerConfiguration() {
082                super();
083        }
084
085        /**
086         * Get the resourceBindings
087         *
088         * @return the resourceBindings
089         */
090        public Collection<ResourceBinding> getResourceBindings() {
091                return myResourceBindings;
092        }
093
094        /**
095         * Set the resourceBindings
096         *
097         * @param resourceBindings the resourceBindings to set
098         */
099        public RestfulServerConfiguration setResourceBindings(Collection<ResourceBinding> resourceBindings) {
100                this.myResourceBindings = resourceBindings;
101                return this;
102        }
103
104        /**
105         * Get the serverBindings
106         *
107         * @return the serverBindings
108         */
109        public List<BaseMethodBinding> getServerBindings() {
110                return myServerBindings;
111        }
112
113        /**
114         * Set the theServerBindings
115         */
116        public RestfulServerConfiguration setServerBindings(List<BaseMethodBinding> theServerBindings) {
117                this.myServerBindings = theServerBindings;
118                return this;
119        }
120
121        public Map<String, Class<? extends IBaseResource>> getNameToSharedSupertype() {
122                return myResourceNameToSharedSupertype;
123        }
124
125        public RestfulServerConfiguration setNameToSharedSupertype(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 = resourceToMethods
252                        .values()
253                        .stream().flatMap(t -> t.stream())
254                        .collect(Collectors.toList());
255                if (myGlobalBindings != null) {
256                        methodBindings.addAll(myGlobalBindings);
257                }
258
259                ListMultimap<String, OperationMethodBinding> nameToOperationMethodBindings = ArrayListMultimap.create();
260                for (BaseMethodBinding nextMethodBinding : methodBindings) {
261                        if (nextMethodBinding instanceof OperationMethodBinding) {
262                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
263                                nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding);
264                        } else if (nextMethodBinding instanceof SearchMethodBinding) {
265                                SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding;
266                                if (namedSearchMethodBindingToName.containsKey(methodBinding)) {
267                                        continue;
268                                }
269
270                                String name = createNamedQueryName(methodBinding);
271                                ourLog.trace("Detected named query: {}", name);
272
273                                namedSearchMethodBindingToName.put(methodBinding, name);
274                                if (!searchNameToBindings.containsKey(name)) {
275                                        searchNameToBindings.put(name, new ArrayList<>());
276                                }
277                                searchNameToBindings.get(name).add(methodBinding);
278                        }
279                }
280
281                for (String nextName : nameToOperationMethodBindings.keySet()) {
282                        List<OperationMethodBinding> nextMethodBindings = nameToOperationMethodBindings.get(nextName);
283
284                        boolean global = false;
285                        boolean system = false;
286                        boolean instance = false;
287                        boolean type = false;
288                        Set<String> resourceTypes = null;
289
290                        for (OperationMethodBinding nextMethodBinding : nextMethodBindings) {
291                                global |= nextMethodBinding.isGlobalMethod();
292                                system |= nextMethodBinding.isCanOperateAtServerLevel();
293                                type |= nextMethodBinding.isCanOperateAtTypeLevel();
294                                instance |= nextMethodBinding.isCanOperateAtInstanceLevel();
295                                if (nextMethodBinding.getResourceName() != null) {
296                                        resourceTypes = resourceTypes != null ? resourceTypes : new TreeSet<>();
297                                        resourceTypes.add(nextMethodBinding.getResourceName());
298                                }
299                        }
300
301                        StringBuilder operationIdBuilder = new StringBuilder();
302                        if (global) {
303                                operationIdBuilder.append("Global");
304                        } else if (resourceTypes != null && resourceTypes.size() == 1) {
305                                operationIdBuilder.append(resourceTypes.iterator().next());
306                        } else if (resourceTypes != null && resourceTypes.size() == 2) {
307                                Iterator<String> iterator = resourceTypes.iterator();
308                                operationIdBuilder.append(iterator.next());
309                                operationIdBuilder.append(iterator.next());
310                        } else if (resourceTypes != null) {
311                                operationIdBuilder.append("Multi");
312                        }
313
314                        operationIdBuilder.append('-');
315                        if (instance) {
316                                operationIdBuilder.append('i');
317                        }
318                        if (type) {
319                                operationIdBuilder.append('t');
320                        }
321                        if (system) {
322                                operationIdBuilder.append('s');
323                        }
324                        operationIdBuilder.append('-');
325
326                        // Exclude the leading $
327                        operationIdBuilder.append(nextName, 1, nextName.length());
328
329                        String operationId = operationIdBuilder.toString();
330                        operationIdToBindings.put(operationId, nextMethodBindings);
331                        nextMethodBindings.forEach(t->operationBindingToId.put(t, operationId));
332                }
333
334                for (BaseMethodBinding nextMethodBinding : methodBindings) {
335                        if (nextMethodBinding instanceof OperationMethodBinding) {
336                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
337                                if (operationBindingToId.containsKey(methodBinding)) {
338                                        continue;
339                                }
340
341                                String name = createOperationName(methodBinding);
342                                ourLog.debug("Detected operation: {}", name);
343
344                                operationBindingToId.put(methodBinding, name);
345                                if (operationIdToBindings.containsKey(name) == false) {
346                                        operationIdToBindings.put(name, new ArrayList<>());
347                                }
348                                operationIdToBindings.get(name).add(methodBinding);
349                        }
350                }
351
352                return new Bindings(namedSearchMethodBindingToName, searchNameToBindings, operationIdToBindings, operationBindingToId);
353        }
354
355        public Map<String, List<BaseMethodBinding>> collectMethodBindings() {
356                Map<String, List<BaseMethodBinding>> resourceToMethods = new TreeMap<>();
357                for (ResourceBinding next : getResourceBindings()) {
358                        String resourceName = next.getResourceName();
359                        for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) {
360                                if (resourceToMethods.containsKey(resourceName) == false) {
361                                        resourceToMethods.put(resourceName, new ArrayList<>());
362                                }
363                                resourceToMethods.get(resourceName).add(nextMethodBinding);
364                        }
365                }
366                for (BaseMethodBinding nextMethodBinding : getServerBindings()) {
367                        String resourceName = "";
368                        if (resourceToMethods.containsKey(resourceName) == false) {
369                                resourceToMethods.put(resourceName, new ArrayList<>());
370                        }
371                        resourceToMethods.get(resourceName).add(nextMethodBinding);
372                }
373                return resourceToMethods;
374        }
375
376        public List<BaseMethodBinding> getGlobalBindings() {
377                return myGlobalBindings;
378        }
379
380        public void setGlobalBindings(List<BaseMethodBinding> theGlobalBindings) {
381                myGlobalBindings = theGlobalBindings;
382        }
383
384        /*
385         * Populates {@link #resourceNameToSharedSupertype} by scanning the given resource providers. Only resource provider getResourceType values
386         * are taken into account. {@link ProvidesResources} and method return types are deliberately ignored.
387         *
388         * Given a resource name, the common superclass for all getResourceType return values for that name's providers is the common superclass
389         * for all returned/received resources with that name. Since {@link ProvidesResources} resources and method return types must also be
390         * subclasses of this common supertype, they can't affect the result of this method.
391         */
392        public void computeSharedSupertypeForResourcePerName(Collection<IResourceProvider> providers) {
393                Map<String, CommonResourceSupertypeScanner> resourceNameToScanner = new HashMap<>();
394
395                List<Class<? extends IBaseResource>> providedResourceClasses = providers.stream()
396                        .map(provider -> provider.getResourceType())
397                        .collect(Collectors.toList());
398                providedResourceClasses.stream()
399                        .forEach(resourceClass -> {
400                                RuntimeResourceDefinition baseDefinition = getFhirContext().getResourceDefinition(resourceClass).getBaseDefinition();
401                                CommonResourceSupertypeScanner scanner = resourceNameToScanner.computeIfAbsent(baseDefinition.getName(), key -> new CommonResourceSupertypeScanner());
402                                scanner.register(resourceClass);
403                        });
404
405                myResourceNameToSharedSupertype = resourceNameToScanner.entrySet().stream()
406                        .filter(entry -> entry.getValue().getLowestCommonSuperclass().isPresent())
407                        .collect(Collectors.toMap(
408                                entry -> entry.getKey(),
409                                entry -> entry.getValue().getLowestCommonSuperclass().get()));
410        }
411
412        private String createNamedQueryName(SearchMethodBinding searchMethodBinding) {
413                StringBuilder retVal = new StringBuilder();
414                if (searchMethodBinding.getResourceName() != null) {
415                        retVal.append(searchMethodBinding.getResourceName());
416                }
417                retVal.append("-query-");
418                retVal.append(searchMethodBinding.getQueryName());
419
420                return retVal.toString();
421        }
422
423        @Override
424        public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
425                return getActiveSearchParams(theResourceName).get(theParamName);
426        }
427
428        @Override
429        public ResourceSearchParams getActiveSearchParams(@Nonnull String theResourceName) {
430                Validate.notBlank(theResourceName, "theResourceName must not be null or blank");
431
432                ResourceSearchParams retval = new ResourceSearchParams(theResourceName);
433
434                collectMethodBindings()
435                        .getOrDefault(theResourceName, Collections.emptyList())
436                        .stream()
437                        .filter(t -> theResourceName.equals(t.getResourceName()))
438                        .filter(t -> t instanceof SearchMethodBinding)
439                        .map(t -> (SearchMethodBinding) t)
440                        .filter(t -> t.getQueryName() == null)
441                        .forEach(t -> createRuntimeBinding(retval, t));
442
443                return retval;
444        }
445
446        @Nullable
447        @Override
448        public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) {
449                throw new UnsupportedOperationException(Msg.code(286));
450        }
451
452        private void createRuntimeBinding(ResourceSearchParams theMapToPopulate, SearchMethodBinding theSearchMethodBinding) {
453
454                List<SearchParameter> parameters = theSearchMethodBinding
455                        .getParameters()
456                        .stream()
457                        .filter(t -> t instanceof SearchParameter)
458                        .map(t -> (SearchParameter) t)
459                        .sorted(SearchParameterComparator.INSTANCE)
460                        .collect(Collectors.toList());
461
462                for (SearchParameter nextParameter : parameters) {
463
464                        String nextParamName = nextParameter.getName();
465
466                        String nextParamUnchainedName = nextParamName;
467                        if (nextParamName.contains(".")) {
468                                nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
469                        }
470
471                        String nextParamDescription = nextParameter.getDescription();
472
473                        /*
474                         * If the parameter has no description, default to the one from the resource
475                         */
476                        if (StringUtils.isBlank(nextParamDescription)) {
477                                RuntimeResourceDefinition def = getFhirContext().getResourceDefinition(theSearchMethodBinding.getResourceName());
478                                RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
479                                if (paramDef != null) {
480                                        nextParamDescription = paramDef.getDescription();
481                                }
482                        }
483
484                        if (theMapToPopulate.containsParamName(nextParamUnchainedName)) {
485                                continue;
486                        }
487
488                        IIdType id = getFhirContext().getVersion().newIdType().setValue("SearchParameter/" + theSearchMethodBinding.getResourceName() + "-" + nextParamName);
489                        String uri = null;
490                        String description = nextParamDescription;
491                        String path = null;
492                        RestSearchParameterTypeEnum type = nextParameter.getParamType();
493                        Set<String> providesMembershipInCompartments = Collections.emptySet();
494                        Set<String> targets = Collections.emptySet();
495                        RuntimeSearchParam.RuntimeSearchParamStatusEnum status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
496                        Collection<String> base = Collections.singletonList(theSearchMethodBinding.getResourceName());
497                        RuntimeSearchParam param = new RuntimeSearchParam(id, uri, nextParamName, description, path, type, providesMembershipInCompartments, targets, status, null, null, base);
498                        theMapToPopulate.put(nextParamName, param);
499
500                }
501
502        }
503
504        private static class SearchParameterComparator implements Comparator<SearchParameter> {
505                private static final SearchParameterComparator INSTANCE = new SearchParameterComparator();
506
507                @Override
508                public int compare(SearchParameter theO1, SearchParameter theO2) {
509                        if (theO1.isRequired() == theO2.isRequired()) {
510                                return theO1.getName().compareTo(theO2.getName());
511                        }
512                        if (theO1.isRequired()) {
513                                return -1;
514                        }
515                        return 1;
516                }
517        }
518
519        private static String createOperationName(OperationMethodBinding theMethodBinding) {
520                StringBuilder retVal = new StringBuilder();
521                if (theMethodBinding.getResourceName() != null) {
522                        retVal.append(theMethodBinding.getResourceName());
523                } else if (theMethodBinding.isGlobalMethod()) {
524                        retVal.append("Global");
525                }
526
527                retVal.append('-');
528                if (theMethodBinding.isCanOperateAtInstanceLevel()) {
529                        retVal.append('i');
530                }
531                if (theMethodBinding.isCanOperateAtServerLevel()) {
532                        retVal.append('s');
533                }
534                retVal.append('-');
535
536                // Exclude the leading $
537                retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
538
539                return retVal.toString();
540        }
541}