001/*
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2023 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 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.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(Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype) {
125                this.myResourceNameToSharedSupertype = resourceNameToSharedSupertype;
126                return this;
127        }
128
129        /**
130         * Get the implementationDescription
131         *
132         * @return the implementationDescription
133         */
134        public String getImplementationDescription() {
135                if (isBlank(myImplementationDescription)) {
136                        return "HAPI FHIR";
137                }
138                return myImplementationDescription;
139        }
140
141        /**
142         * Set the implementationDescription
143         *
144         * @param implementationDescription the implementationDescription to set
145         */
146        public RestfulServerConfiguration setImplementationDescription(String implementationDescription) {
147                this.myImplementationDescription = implementationDescription;
148                return this;
149        }
150
151        /**
152         * Get the serverVersion
153         *
154         * @return the serverVersion
155         */
156        public String getServerVersion() {
157                return myServerVersion;
158        }
159
160        /**
161         * Set the serverVersion
162         *
163         * @param serverVersion the serverVersion to set
164         */
165        public RestfulServerConfiguration setServerVersion(String serverVersion) {
166                this.myServerVersion = serverVersion;
167                return this;
168        }
169
170        /**
171         * Get the serverName
172         *
173         * @return the serverName
174         */
175        public String getServerName() {
176                return myServerName;
177        }
178
179        /**
180         * Set the serverName
181         *
182         * @param serverName the serverName to set
183         */
184        public RestfulServerConfiguration setServerName(String serverName) {
185                this.myServerName = serverName;
186                return this;
187        }
188
189        /**
190         * 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
191         * creating their own.
192         */
193        public FhirContext getFhirContext() {
194                return this.myFhirContext;
195        }
196
197        /**
198         * Set the fhirContext
199         *
200         * @param fhirContext the fhirContext to set
201         */
202        public RestfulServerConfiguration setFhirContext(FhirContext fhirContext) {
203                this.myFhirContext = fhirContext;
204                return this;
205        }
206
207        /**
208         * Get the serverAddressStrategy
209         *
210         * @return the serverAddressStrategy
211         */
212        public IServerAddressStrategy getServerAddressStrategy() {
213                return myServerAddressStrategy;
214        }
215
216        /**
217         * Set the serverAddressStrategy
218         *
219         * @param serverAddressStrategy the serverAddressStrategy to set
220         */
221        public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) {
222                this.myServerAddressStrategy = serverAddressStrategy;
223        }
224
225        /**
226         * Get the date that will be specified in the conformance profile
227         * exported by this server. Typically this would be populated with
228         * an InstanceType.
229         */
230        public IPrimitiveType<Date> getConformanceDate() {
231                return myConformanceDate;
232        }
233
234        /**
235         * Set the date that will be specified in the conformance profile
236         * exported by this server. Typically this would be populated with
237         * an InstanceType.
238         */
239        public void setConformanceDate(IPrimitiveType<Date> theConformanceDate) {
240                myConformanceDate = theConformanceDate;
241        }
242
243        public Bindings provideBindings() {
244                IdentityHashMap<SearchMethodBinding, String> namedSearchMethodBindingToName = new IdentityHashMap<>();
245                HashMap<String, List<SearchMethodBinding>> searchNameToBindings = new HashMap<>();
246                IdentityHashMap<OperationMethodBinding, String> operationBindingToId = new IdentityHashMap<>();
247                HashMap<String, List<OperationMethodBinding>> operationIdToBindings = new HashMap<>();
248
249                Map<String, List<BaseMethodBinding>> resourceToMethods = collectMethodBindings();
250                List<BaseMethodBinding> methodBindings = resourceToMethods
251                        .values()
252                        .stream().flatMap(t -> t.stream())
253                        .collect(Collectors.toList());
254                if (myGlobalBindings != null) {
255                        methodBindings.addAll(myGlobalBindings);
256                }
257
258                ListMultimap<String, OperationMethodBinding> nameToOperationMethodBindings = ArrayListMultimap.create();
259                for (BaseMethodBinding nextMethodBinding : methodBindings) {
260                        if (nextMethodBinding instanceof OperationMethodBinding) {
261                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
262                                nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding);
263                        } else if (nextMethodBinding instanceof SearchMethodBinding) {
264                                SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding;
265                                if (namedSearchMethodBindingToName.containsKey(methodBinding)) {
266                                        continue;
267                                }
268
269                                String name = createNamedQueryName(methodBinding);
270                                ourLog.trace("Detected named query: {}", name);
271
272                                namedSearchMethodBindingToName.put(methodBinding, name);
273                                if (!searchNameToBindings.containsKey(name)) {
274                                        searchNameToBindings.put(name, new ArrayList<>());
275                                }
276                                searchNameToBindings.get(name).add(methodBinding);
277                        }
278                }
279
280                for (String nextName : nameToOperationMethodBindings.keySet()) {
281                        List<OperationMethodBinding> nextMethodBindings = nameToOperationMethodBindings.get(nextName);
282
283                        boolean global = false;
284                        boolean system = false;
285                        boolean instance = false;
286                        boolean type = false;
287                        Set<String> resourceTypes = null;
288
289                        for (OperationMethodBinding nextMethodBinding : nextMethodBindings) {
290                                global |= nextMethodBinding.isGlobalMethod();
291                                system |= nextMethodBinding.isCanOperateAtServerLevel();
292                                type |= nextMethodBinding.isCanOperateAtTypeLevel();
293                                instance |= nextMethodBinding.isCanOperateAtInstanceLevel();
294                                if (nextMethodBinding.getResourceName() != null) {
295                                        resourceTypes = resourceTypes != null ? resourceTypes : new TreeSet<>();
296                                        resourceTypes.add(nextMethodBinding.getResourceName());
297                                }
298                        }
299
300                        StringBuilder operationIdBuilder = new StringBuilder();
301                        if (global) {
302                                operationIdBuilder.append("Global");
303                        } else if (resourceTypes != null && resourceTypes.size() == 1) {
304                                operationIdBuilder.append(resourceTypes.iterator().next());
305                        } else if (resourceTypes != null && resourceTypes.size() == 2) {
306                                Iterator<String> iterator = resourceTypes.iterator();
307                                operationIdBuilder.append(iterator.next());
308                                operationIdBuilder.append(iterator.next());
309                        } else if (resourceTypes != null) {
310                                operationIdBuilder.append("Multi");
311                        }
312
313                        operationIdBuilder.append('-');
314                        if (instance) {
315                                operationIdBuilder.append('i');
316                        }
317                        if (type) {
318                                operationIdBuilder.append('t');
319                        }
320                        if (system) {
321                                operationIdBuilder.append('s');
322                        }
323                        operationIdBuilder.append('-');
324
325                        // Exclude the leading $
326                        operationIdBuilder.append(nextName, 1, nextName.length());
327
328                        String operationId = operationIdBuilder.toString();
329                        operationIdToBindings.put(operationId, nextMethodBindings);
330                        nextMethodBindings.forEach(t->operationBindingToId.put(t, operationId));
331                }
332
333                for (BaseMethodBinding nextMethodBinding : methodBindings) {
334                        if (nextMethodBinding instanceof OperationMethodBinding) {
335                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
336                                if (operationBindingToId.containsKey(methodBinding)) {
337                                        continue;
338                                }
339
340                                String name = createOperationName(methodBinding);
341                                ourLog.debug("Detected operation: {}", name);
342
343                                operationBindingToId.put(methodBinding, name);
344                                if (operationIdToBindings.containsKey(name) == false) {
345                                        operationIdToBindings.put(name, new ArrayList<>());
346                                }
347                                operationIdToBindings.get(name).add(methodBinding);
348                        }
349                }
350
351                return new Bindings(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 = providers.stream()
395                        .map(provider -> provider.getResourceType())
396                        .collect(Collectors.toList());
397                providedResourceClasses.stream()
398                        .forEach(resourceClass -> {
399                                RuntimeResourceDefinition baseDefinition = getFhirContext().getResourceDefinition(resourceClass).getBaseDefinition();
400                                CommonResourceSupertypeScanner scanner = resourceNameToScanner.computeIfAbsent(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(String theResourceName, String theParamName) {
424                return getActiveSearchParams(theResourceName).get(theParamName);
425        }
426
427        @Override
428        public ResourceSearchParams getActiveSearchParams(@Nonnull String theResourceName) {
429                Validate.notBlank(theResourceName, "theResourceName must not be null or blank");
430
431                ResourceSearchParams retval = new ResourceSearchParams(theResourceName);
432
433                collectMethodBindings()
434                        .getOrDefault(theResourceName, Collections.emptyList())
435                        .stream()
436                        .filter(t -> theResourceName.equals(t.getResourceName()))
437                        .filter(t -> t instanceof SearchMethodBinding)
438                        .map(t -> (SearchMethodBinding) t)
439                        .filter(t -> t.getQueryName() == null)
440                        .forEach(t -> createRuntimeBinding(retval, t));
441
442                return retval;
443        }
444
445        @Nullable
446        @Override
447        public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) {
448                throw new UnsupportedOperationException(Msg.code(286));
449        }
450
451        private void createRuntimeBinding(ResourceSearchParams theMapToPopulate, SearchMethodBinding theSearchMethodBinding) {
452
453                List<SearchParameter> parameters = theSearchMethodBinding
454                        .getParameters()
455                        .stream()
456                        .filter(t -> t instanceof SearchParameter)
457                        .map(t -> (SearchParameter) t)
458                        .sorted(SearchParameterComparator.INSTANCE)
459                        .collect(Collectors.toList());
460
461                for (SearchParameter nextParameter : parameters) {
462
463                        String nextParamName = nextParameter.getName();
464
465                        String nextParamUnchainedName = nextParamName;
466                        if (nextParamName.contains(".")) {
467                                nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
468                        }
469
470                        String nextParamDescription = nextParameter.getDescription();
471
472                        /*
473                         * If the parameter has no description, default to the one from the resource
474                         */
475                        if (StringUtils.isBlank(nextParamDescription)) {
476                                RuntimeResourceDefinition def = getFhirContext().getResourceDefinition(theSearchMethodBinding.getResourceName());
477                                RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
478                                if (paramDef != null) {
479                                        nextParamDescription = paramDef.getDescription();
480                                }
481                        }
482
483                        if (theMapToPopulate.containsParamName(nextParamUnchainedName)) {
484                                continue;
485                        }
486
487                        IIdType id = getFhirContext().getVersion().newIdType().setValue("SearchParameter/" + theSearchMethodBinding.getResourceName() + "-" + nextParamName);
488                        String uri = null;
489                        String description = nextParamDescription;
490                        String path = null;
491                        RestSearchParameterTypeEnum type = nextParameter.getParamType();
492                        Set<String> providesMembershipInCompartments = Collections.emptySet();
493                        Set<String> targets = Collections.emptySet();
494                        RuntimeSearchParam.RuntimeSearchParamStatusEnum status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE;
495                        Collection<String> base = Collections.singletonList(theSearchMethodBinding.getResourceName());
496                        RuntimeSearchParam param = new RuntimeSearchParam(id, uri, nextParamName, description, path, type, providesMembershipInCompartments, targets, status, null, null, base);
497                        theMapToPopulate.put(nextParamName, param);
498
499                }
500
501        }
502
503        private static class SearchParameterComparator implements Comparator<SearchParameter> {
504                private static final SearchParameterComparator INSTANCE = new SearchParameterComparator();
505
506                @Override
507                public int compare(SearchParameter theO1, SearchParameter theO2) {
508                        if (theO1.isRequired() == theO2.isRequired()) {
509                                return theO1.getName().compareTo(theO2.getName());
510                        }
511                        if (theO1.isRequired()) {
512                                return -1;
513                        }
514                        return 1;
515                }
516        }
517
518        private static String createOperationName(OperationMethodBinding theMethodBinding) {
519                StringBuilder retVal = new StringBuilder();
520                if (theMethodBinding.getResourceName() != null) {
521                        retVal.append(theMethodBinding.getResourceName());
522                } else if (theMethodBinding.isGlobalMethod()) {
523                        retVal.append("Global");
524                }
525
526                retVal.append('-');
527                if (theMethodBinding.isCanOperateAtInstanceLevel()) {
528                        retVal.append('i');
529                }
530                if (theMethodBinding.isCanOperateAtServerLevel()) {
531                        retVal.append('s');
532                }
533                retVal.append('-');
534
535                // Exclude the leading $
536                retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
537
538                return retVal.toString();
539        }
540}