001package ca.uhn.fhir.rest.server;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
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.rest.server.method.BaseMethodBinding;
025import ca.uhn.fhir.rest.server.method.OperationMethodBinding;
026import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
027import ca.uhn.fhir.util.VersionUtil;
028import org.hl7.fhir.instance.model.api.IPrimitiveType;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import java.util.*;
033
034import static org.apache.commons.lang3.StringUtils.isBlank;
035
036import ca.uhn.fhir.context.RuntimeResourceDefinition;
037import java.util.stream.Collectors;
038import org.hl7.fhir.instance.model.api.IBaseResource;
039
040public class RestfulServerConfiguration {
041
042        private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerConfiguration.class);
043        private Collection<ResourceBinding> resourceBindings;
044        private List<BaseMethodBinding<?>> serverBindings;
045    private Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype;
046        private String implementationDescription;
047        private String serverVersion = VersionUtil.getVersion();
048        private String serverName = "HAPI FHIR";
049        private FhirContext fhirContext;
050        private IServerAddressStrategy serverAddressStrategy;
051        private IPrimitiveType<Date> myConformanceDate;
052
053        /**
054         * Constructor
055         */
056        public RestfulServerConfiguration() {
057                super();
058        }
059
060        /**
061         * Get the resourceBindings
062         *
063         * @return the resourceBindings
064         */
065        public Collection<ResourceBinding> getResourceBindings() {
066                return resourceBindings;
067        }
068
069        /**
070         * Set the resourceBindings
071         *
072         * @param resourceBindings the resourceBindings to set
073         */
074        public RestfulServerConfiguration setResourceBindings(Collection<ResourceBinding> resourceBindings) {
075                this.resourceBindings = resourceBindings;
076                return this;
077        }
078
079        /**
080         * Get the serverBindings
081         *
082         * @return the serverBindings
083         */
084        public List<BaseMethodBinding<?>> getServerBindings() {
085                return serverBindings;
086        }
087
088        /**
089         * Set the theServerBindings
090         */
091        public RestfulServerConfiguration setServerBindings(List<BaseMethodBinding<?>> theServerBindings) {
092                this.serverBindings = theServerBindings;
093                return this;
094        }
095    
096    public Map<String, Class<? extends IBaseResource>> getNameToSharedSupertype() {
097      return resourceNameToSharedSupertype;
098    }
099
100    public RestfulServerConfiguration setNameToSharedSupertype(Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype) {
101      this.resourceNameToSharedSupertype = resourceNameToSharedSupertype;
102      return this;
103    }
104
105        /**
106         * Get the implementationDescription
107         *
108         * @return the implementationDescription
109         */
110        public String getImplementationDescription() {
111                if (isBlank(implementationDescription)) {
112                        return "HAPI FHIR";
113                }
114                return implementationDescription;
115        }
116
117        /**
118         * Set the implementationDescription
119         *
120         * @param implementationDescription the implementationDescription to set
121         */
122        public RestfulServerConfiguration setImplementationDescription(String implementationDescription) {
123                this.implementationDescription = implementationDescription;
124                return this;
125        }
126
127        /**
128         * Get the serverVersion
129         *
130         * @return the serverVersion
131         */
132        public String getServerVersion() {
133                return serverVersion;
134        }
135
136        /**
137         * Set the serverVersion
138         *
139         * @param serverVersion the serverVersion to set
140         */
141        public RestfulServerConfiguration setServerVersion(String serverVersion) {
142                this.serverVersion = serverVersion;
143                return this;
144        }
145
146        /**
147         * Get the serverName
148         *
149         * @return the serverName
150         */
151        public String getServerName() {
152                return serverName;
153        }
154
155        /**
156         * Set the serverName
157         *
158         * @param serverName the serverName to set
159         */
160        public RestfulServerConfiguration setServerName(String serverName) {
161                this.serverName = serverName;
162                return this;
163        }
164
165        /**
166         * 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
167         * creating their own.
168         */
169        public FhirContext getFhirContext() {
170                return this.fhirContext;
171        }
172
173        /**
174         * Set the fhirContext
175         *
176         * @param fhirContext the fhirContext to set
177         */
178        public RestfulServerConfiguration setFhirContext(FhirContext fhirContext) {
179                this.fhirContext = fhirContext;
180                return this;
181        }
182
183        /**
184         * Get the serverAddressStrategy
185         *
186         * @return the serverAddressStrategy
187         */
188        public IServerAddressStrategy getServerAddressStrategy() {
189                return serverAddressStrategy;
190        }
191
192        /**
193         * Set the serverAddressStrategy
194         *
195         * @param serverAddressStrategy the serverAddressStrategy to set
196         */
197        public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) {
198                this.serverAddressStrategy = serverAddressStrategy;
199        }
200
201        /**
202         * Get the date that will be specified in the conformance profile
203         * exported by this server. Typically this would be populated with
204         * an InstanceType.
205         */
206        public IPrimitiveType<Date> getConformanceDate() {
207                return myConformanceDate;
208        }
209
210        /**
211         * Set the date that will be specified in the conformance profile
212         * exported by this server. Typically this would be populated with
213         * an InstanceType.
214         */
215        public void setConformanceDate(IPrimitiveType<Date> theConformanceDate) {
216                myConformanceDate = theConformanceDate;
217        }
218
219        public Bindings provideBindings() {
220                IdentityHashMap<SearchMethodBinding, String> myNamedSearchMethodBindingToName = new IdentityHashMap<>();
221                HashMap<String, List<SearchMethodBinding>> mySearchNameToBindings = new HashMap<>();
222                IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName = new IdentityHashMap<>();
223                HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings = new HashMap<>();
224
225                Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
226                for (Map.Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
227                        List<BaseMethodBinding<?>> nextMethodBindings = nextEntry.getValue();
228                        for (BaseMethodBinding<?> nextMethodBinding : nextMethodBindings) {
229                                if (nextMethodBinding instanceof OperationMethodBinding) {
230                                        OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
231                                        if (myOperationBindingToName.containsKey(methodBinding)) {
232                                                continue;
233                                        }
234
235                                        String name = createOperationName(methodBinding);
236                                        ourLog.debug("Detected operation: {}", name);
237
238                                        myOperationBindingToName.put(methodBinding, name);
239                                        if (myOperationNameToBindings.containsKey(name) == false) {
240                                                myOperationNameToBindings.put(name, new ArrayList<>());
241                                        }
242                                        myOperationNameToBindings.get(name).add(methodBinding);
243                                } else if (nextMethodBinding instanceof SearchMethodBinding) {
244                                        SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding;
245                                        if (myNamedSearchMethodBindingToName.containsKey(methodBinding)) {
246                                                continue;
247                                        }
248
249                                        String name = createNamedQueryName(methodBinding);
250                                        ourLog.debug("Detected named query: {}", name);
251
252                                        myNamedSearchMethodBindingToName.put(methodBinding, name);
253                                        if (!mySearchNameToBindings.containsKey(name)) {
254                                                mySearchNameToBindings.put(name, new ArrayList<>());
255                                        }
256                                        mySearchNameToBindings.get(name).add(methodBinding);
257                                }
258                        }
259                }
260
261                return new Bindings(myNamedSearchMethodBindingToName, mySearchNameToBindings, myOperationNameToBindings, myOperationBindingToName);
262        }
263
264        public Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
265                Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
266                for (ResourceBinding next : getResourceBindings()) {
267                        String resourceName = next.getResourceName();
268                        for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
269                                if (resourceToMethods.containsKey(resourceName) == false) {
270                                        resourceToMethods.put(resourceName, new ArrayList<>());
271                                }
272                                resourceToMethods.get(resourceName).add(nextMethodBinding);
273                        }
274                }
275                for (BaseMethodBinding<?> nextMethodBinding : getServerBindings()) {
276                        String resourceName = "";
277                        if (resourceToMethods.containsKey(resourceName) == false) {
278                                resourceToMethods.put(resourceName, new ArrayList<>());
279                        }
280                        resourceToMethods.get(resourceName).add(nextMethodBinding);
281                }
282                return resourceToMethods;
283        }
284
285        /*
286         * Populates {@link #resourceNameToSharedSupertype} by scanning the given resource providers. Only resource provider getResourceType values
287         * are taken into account. {@link ProvidesResources} and method return types are deliberately ignored.
288         *
289         * Given a resource name, the common superclass for all getResourceType return values for that name's providers is the common superclass
290         * for all returned/received resources with that name. Since {@link ProvidesResources} resources and method return types must also be
291         * subclasses of this common supertype, they can't affect the result of this method.
292         */
293        public void computeSharedSupertypeForResourcePerName(Collection<IResourceProvider> providers) {
294                Map<String, CommonResourceSupertypeScanner> resourceNameToScanner = new HashMap<>();
295
296                List<Class<? extends IBaseResource>> providedResourceClasses = providers.stream()
297                        .map(provider -> provider.getResourceType())
298                        .collect(Collectors.toList());
299                providedResourceClasses.stream()
300                        .forEach(resourceClass -> {
301                                RuntimeResourceDefinition baseDefinition = getFhirContext().getResourceDefinition(resourceClass).getBaseDefinition();
302                                CommonResourceSupertypeScanner scanner = resourceNameToScanner.computeIfAbsent(baseDefinition.getName(), key -> new CommonResourceSupertypeScanner());
303                                scanner.register(resourceClass);
304                        });
305
306                resourceNameToSharedSupertype = resourceNameToScanner.entrySet().stream()
307                        .filter(entry -> entry.getValue().getLowestCommonSuperclass().isPresent())
308                        .collect(Collectors.toMap(
309                                entry -> entry.getKey(),
310                                entry -> entry.getValue().getLowestCommonSuperclass().get()));
311        }
312
313        private String createOperationName(OperationMethodBinding theMethodBinding) {
314                StringBuilder retVal = new StringBuilder();
315                if (theMethodBinding.getResourceName() != null) {
316                        retVal.append(theMethodBinding.getResourceName());
317                }
318
319                retVal.append('-');
320                if (theMethodBinding.isCanOperateAtInstanceLevel()) {
321                        retVal.append('i');
322                }
323                if (theMethodBinding.isCanOperateAtServerLevel()) {
324                        retVal.append('s');
325                }
326                retVal.append('-');
327
328                // Exclude the leading $
329                retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
330
331                return retVal.toString();
332        }
333
334
335        private String createNamedQueryName(SearchMethodBinding searchMethodBinding) {
336                StringBuilder retVal = new StringBuilder();
337                if (searchMethodBinding.getResourceName() != null) {
338                        retVal.append(searchMethodBinding.getResourceName());
339                }
340                retVal.append("-query-");
341                retVal.append(searchMethodBinding.getQueryName());
342
343                return retVal.toString();
344        }
345
346}