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