001package ca.uhn.fhir.rest.server.provider;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.context.RuntimeResourceDefinition;
005import ca.uhn.fhir.context.RuntimeSearchParam;
006import ca.uhn.fhir.context.support.IValidationSupport;
007import ca.uhn.fhir.i18n.Msg;
008import ca.uhn.fhir.model.primitive.InstantDt;
009import ca.uhn.fhir.parser.DataFormatException;
010import ca.uhn.fhir.rest.annotation.IdParam;
011import ca.uhn.fhir.rest.annotation.Metadata;
012import ca.uhn.fhir.rest.annotation.Read;
013import ca.uhn.fhir.rest.api.Constants;
014import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
015import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
016import ca.uhn.fhir.rest.api.server.RequestDetails;
017import ca.uhn.fhir.rest.server.Bindings;
018import ca.uhn.fhir.rest.server.IServerConformanceProvider;
019import ca.uhn.fhir.rest.server.RestfulServer;
020import ca.uhn.fhir.rest.server.RestfulServerConfiguration;
021import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
022import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
023import ca.uhn.fhir.rest.server.method.IParameter;
024import ca.uhn.fhir.rest.server.method.OperationMethodBinding;
025import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType;
026import ca.uhn.fhir.rest.server.method.OperationParameter;
027import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
028import ca.uhn.fhir.rest.server.method.SearchParameter;
029import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
030import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
031import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
032import ca.uhn.fhir.util.ExtensionUtil;
033import ca.uhn.fhir.util.FhirTerser;
034import ca.uhn.fhir.util.HapiExtensions;
035import com.google.common.collect.TreeMultimap;
036import org.apache.commons.text.WordUtils;
037import org.hl7.fhir.instance.model.api.IBase;
038import org.hl7.fhir.instance.model.api.IBaseConformance;
039import org.hl7.fhir.instance.model.api.IBaseExtension;
040import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
041import org.hl7.fhir.instance.model.api.IBaseResource;
042import org.hl7.fhir.instance.model.api.IIdType;
043import org.hl7.fhir.instance.model.api.IPrimitiveType;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import javax.annotation.Nonnull;
048import javax.servlet.ServletContext;
049import javax.servlet.http.HttpServletRequest;
050import java.util.Date;
051import java.util.HashMap;
052import java.util.HashSet;
053import java.util.List;
054import java.util.Map;
055import java.util.Map.Entry;
056import java.util.NavigableSet;
057import java.util.Set;
058import java.util.TreeSet;
059import java.util.UUID;
060import java.util.stream.Collectors;
061
062import static org.apache.commons.lang3.StringUtils.defaultString;
063import static org.apache.commons.lang3.StringUtils.isBlank;
064import static org.apache.commons.lang3.StringUtils.isNotBlank;
065
066/*
067 * #%L
068 * HAPI FHIR - Server Framework
069 * %%
070 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
071 * %%
072 * Licensed under the Apache License, Version 2.0 (the "License");
073 * you may not use this file except in compliance with the License.
074 * You may obtain a copy of the License at
075 *
076 *      http://www.apache.org/licenses/LICENSE-2.0
077 *
078 * Unless required by applicable law or agreed to in writing, software
079 * distributed under the License is distributed on an "AS IS" BASIS,
080 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
081 * See the License for the specific language governing permissions and
082 * limitations under the License.
083 * #L%
084 */
085
086/**
087 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation
088 * <p>
089 * This class is version independent, but will only work on servers supporting FHIR R4+ (as this was
090 * the first FHIR release where CapabilityStatement was a normative resource)
091 */
092public class ServerCapabilityStatementProvider implements IServerConformanceProvider<IBaseConformance> {
093
094        public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true;
095        private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class);
096        private final FhirContext myContext;
097        private final RestfulServer myServer;
098        private final ISearchParamRegistry mySearchParamRegistry;
099        private final RestfulServerConfiguration myServerConfiguration;
100        private final IValidationSupport myValidationSupport;
101        private String myPublisher = "Not provided";
102        private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED;
103
104        /**
105         * Constructor
106         */
107        public ServerCapabilityStatementProvider(RestfulServer theServer) {
108                myServer = theServer;
109                myContext = theServer.getFhirContext();
110                mySearchParamRegistry = null;
111                myServerConfiguration = null;
112                myValidationSupport = null;
113        }
114
115        /**
116         * Constructor
117         */
118        public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) {
119                myContext = theContext;
120                myServerConfiguration = theServerConfiguration;
121                mySearchParamRegistry = null;
122                myServer = null;
123                myValidationSupport = null;
124        }
125
126        /**
127         * Constructor
128         */
129        public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) {
130                myContext = theRestfulServer.getFhirContext();
131                mySearchParamRegistry = theSearchParamRegistry;
132                myServer = theRestfulServer;
133                myServerConfiguration = null;
134                myValidationSupport = theValidationSupport;
135        }
136
137        private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set<String> theSystemOps, BaseMethodBinding<?> theMethodBinding) {
138                RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType();
139                if (restOperationType.isSystemLevel()) {
140                        String sysOp = restOperationType.getCode();
141                        if (theSystemOps.contains(sysOp) == false) {
142                                theSystemOps.add(sysOp);
143                                IBase interaction = theTerser.addElement(theRest, "interaction");
144                                theTerser.addElement(interaction, "code", sysOp);
145                        }
146                }
147        }
148
149
150        private String conformanceDate(RestfulServerConfiguration theServerConfiguration) {
151                IPrimitiveType<Date> buildDate = theServerConfiguration.getConformanceDate();
152                if (buildDate != null && buildDate.getValue() != null) {
153                        try {
154                                return buildDate.getValueAsString();
155                        } catch (DataFormatException e) {
156                                // fall through
157                        }
158                }
159                return InstantDt.withCurrentTime().getValueAsString();
160        }
161
162        private RestfulServerConfiguration getServerConfiguration() {
163                if (myServer != null) {
164                        return myServer.createConfiguration();
165                }
166                return myServerConfiguration;
167        }
168
169
170        /**
171         * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The
172         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
173         */
174        public String getPublisher() {
175                return myPublisher;
176        }
177
178        /**
179         * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The
180         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
181         */
182        public void setPublisher(String thePublisher) {
183                myPublisher = thePublisher;
184        }
185
186        @Override
187        @Metadata
188        public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
189
190                HttpServletRequest servletRequest = null;
191                if (theRequestDetails instanceof ServletRequestDetails) {
192                        servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest();
193                }
194
195                RestfulServerConfiguration configuration = getServerConfiguration();
196                Bindings bindings = configuration.provideBindings();
197
198                IBaseConformance retVal = (IBaseConformance) myContext.getResourceDefinition("CapabilityStatement").newInstance();
199
200                FhirTerser terser = myContext.newTerser();
201
202                TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser);
203
204                terser.addElement(retVal, "id", UUID.randomUUID().toString());
205                terser.addElement(retVal, "name", "RestServer");
206                terser.addElement(retVal, "publisher", myPublisher);
207                terser.addElement(retVal, "date", conformanceDate(configuration));
208                terser.addElement(retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString());
209
210                ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE));
211                String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest);
212                terser.addElement(retVal, "implementation.url", serverBase);
213                terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription());
214                terser.addElement(retVal, "kind", "instance");
215                if (myServer != null && isNotBlank(myServer.getCopyright())) {
216                        terser.addElement(retVal, "copyright", myServer.getCopyright());
217                }
218                terser.addElement(retVal, "software.name", configuration.getServerName());
219                terser.addElement(retVal, "software.version", configuration.getServerVersion());
220                if (myContext.isFormatXmlSupported()) {
221                        terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW);
222                        terser.addElement(retVal, "format", Constants.FORMAT_XML);
223                }
224                if (myContext.isFormatJsonSupported()) {
225                        terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW);
226                        terser.addElement(retVal, "format", Constants.FORMAT_JSON);
227                }
228                if (myContext.isFormatRdfSupported()) {
229                        terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE);
230                        terser.addElement(retVal, "format", Constants.FORMAT_TURTLE);
231                }
232                terser.addElement(retVal, "status", "active");
233
234                IBase rest = terser.addElement(retVal, "rest");
235                terser.addElement(rest, "mode", "server");
236
237                Set<String> systemOps = new HashSet<>();
238
239                Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings();
240                Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype();
241                List<BaseMethodBinding<?>> globalMethodBindings = configuration.getGlobalBindings();
242
243                TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create();
244                TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create();
245                for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
246                        String resourceName = nextEntry.getKey();
247                        for (BaseMethodBinding<?> nextMethod : nextEntry.getValue()) {
248                                if (nextMethod instanceof SearchMethodBinding) {
249                                        resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes());
250                                        resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes());
251                                }
252                        }
253
254                }
255
256                for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
257
258                        Set<String> operationNames = new HashSet<>();
259                        String resourceName = nextEntry.getKey();
260                        if (nextEntry.getKey().isEmpty() == false) {
261                                Set<String> resourceOps = new HashSet<>();
262                                IBase resource = terser.addElement(rest, "resource");
263
264                                postProcessRestResource(terser, resource, resourceName);
265
266                                RuntimeResourceDefinition def;
267                                FhirContext context = configuration.getFhirContext();
268                                if (resourceNameToSharedSupertype.containsKey(resourceName)) {
269                                        def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName));
270                                } else {
271                                        def = context.getResourceDefinition(resourceName);
272                                }
273                                terser.addElement(resource, "type", def.getName());
274                                terser.addElement(resource, "profile", def.getResourceProfile(serverBase));
275
276                                for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
277                                        RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType();
278                                        if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) {
279                                                String resOp;
280                                                resOp = resOpCode.getCode();
281                                                if (resourceOps.contains(resOp) == false) {
282                                                        resourceOps.add(resOp);
283                                                        IBase interaction = terser.addElement(resource, "interaction");
284                                                        terser.addElement(interaction, "code", resOp);
285                                                }
286                                                if (RestOperationTypeEnum.VREAD.equals(resOpCode)) {
287                                                        // vread implies read
288                                                        resOp = "read";
289                                                        if (resourceOps.contains(resOp) == false) {
290                                                                resourceOps.add(resOp);
291                                                                IBase interaction = terser.addElement(resource, "interaction");
292                                                                terser.addElement(interaction, "code", resOp);
293                                                        }
294                                                }
295                                        }
296
297                                        if (nextMethodBinding.isSupportsConditional()) {
298                                                switch (resOpCode) {
299                                                        case CREATE:
300                                                                terser.setElement(resource, "conditionalCreate", "true");
301                                                                break;
302                                                        case DELETE:
303                                                                if (nextMethodBinding.isSupportsConditionalMultiple()) {
304                                                                        terser.setElement(resource, "conditionalDelete", "multiple");
305                                                                } else {
306                                                                        terser.setElement(resource, "conditionalDelete", "single");
307                                                                }
308                                                                break;
309                                                        case UPDATE:
310                                                                terser.setElement(resource, "conditionalUpdate", "true");
311                                                                break;
312                                                        case HISTORY_INSTANCE:
313                                                        case HISTORY_SYSTEM:
314                                                        case HISTORY_TYPE:
315                                                        case READ:
316                                                        case SEARCH_SYSTEM:
317                                                        case SEARCH_TYPE:
318                                                        case TRANSACTION:
319                                                        case VALIDATE:
320                                                        case VREAD:
321                                                        case METADATA:
322                                                        case META_ADD:
323                                                        case META:
324                                                        case META_DELETE:
325                                                        case PATCH:
326                                                        case BATCH:
327                                                        case ADD_TAGS:
328                                                        case DELETE_TAGS:
329                                                        case GET_TAGS:
330                                                        case GET_PAGE:
331                                                        case GRAPHQL_REQUEST:
332                                                        case EXTENDED_OPERATION_SERVER:
333                                                        case EXTENDED_OPERATION_TYPE:
334                                                        case EXTENDED_OPERATION_INSTANCE:
335                                                        default:
336                                                                break;
337                                                }
338                                        }
339
340                                        checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
341
342                                        // Resource Operations
343                                        if (nextMethodBinding instanceof SearchMethodBinding) {
344                                                addSearchMethodIfSearchIsNamedQuery(theRequestDetails, bindings, terser, operationNames, resource, (SearchMethodBinding) nextMethodBinding);
345                                        } else if (nextMethodBinding instanceof OperationMethodBinding) {
346                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
347                                                String opName = bindings.getOperationBindingToId().get(methodBinding);
348                                                // Only add each operation (by name) once
349                                                if (operationNames.add(opName)) {
350                                                        IBase operation = terser.addElement(resource, "operation");
351                                                        populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
352                                                }
353                                        }
354
355                                }
356
357                                // Find any global operations (Operations defines at the system level but with the
358                                // global flag set to true, meaning they apply to all resource types)
359                                if (globalMethodBindings != null) {
360                                        Set<String> globalOperationNames = new HashSet<>();
361                                        for (BaseMethodBinding<?> next : globalMethodBindings) {
362                                                if (next instanceof OperationMethodBinding) {
363                                                        OperationMethodBinding methodBinding = (OperationMethodBinding) next;
364                                                        if (methodBinding.isGlobalMethod()) {
365                                                                if (methodBinding.isCanOperateAtInstanceLevel() || methodBinding.isCanOperateAtTypeLevel()) {
366                                                                        String opName = bindings.getOperationBindingToId().get(methodBinding);
367                                                                        // Only add each operation (by name) once
368                                                                        if (globalOperationNames.add(opName)) {
369                                                                                IBase operation = terser.addElement(resource, "operation");
370                                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
371                                                                        }
372                                                                }
373                                                        }
374                                                }
375                                        }
376                                }
377
378                                ISearchParamRegistry serverConfiguration;
379                                if (myServerConfiguration != null) {
380                                        serverConfiguration = myServerConfiguration;
381                                } else {
382                                        serverConfiguration = myServer.createConfiguration();
383                                }
384
385                                /*
386                                 * If we have an explicit registry (which will be the case in the JPA server) we use it as priority,
387                                 * but also fill in any gaps using params from the server itself. This makes sure we include
388                                 * global params like _lastUpdated
389                                 */
390                                ResourceSearchParams searchParams;
391                                ISearchParamRegistry searchParamRegistry;
392                                ResourceSearchParams serverConfigurationActiveSearchParams = serverConfiguration.getActiveSearchParams(resourceName);
393                                if (mySearchParamRegistry != null) {
394                                        searchParamRegistry = mySearchParamRegistry;
395                                        searchParams = mySearchParamRegistry.getActiveSearchParams(resourceName).makeCopy();
396                                        for (String nextBuiltInSpName : serverConfigurationActiveSearchParams.getSearchParamNames()) {
397                                                if (nextBuiltInSpName.startsWith("_") &&
398                                                        !searchParams.containsParamName(nextBuiltInSpName) &&
399                                                        searchParamEnabled(nextBuiltInSpName)) {
400                                                        searchParams.put(nextBuiltInSpName, serverConfigurationActiveSearchParams.get(nextBuiltInSpName));
401                                                }
402                                        }
403                                } else {
404                                        searchParamRegistry = serverConfiguration;
405                                        searchParams = serverConfigurationActiveSearchParams;
406                                }
407
408
409                                for (RuntimeSearchParam next : searchParams.values()) {
410                                        IBase searchParam = terser.addElement(resource, "searchParam");
411                                        terser.addElement(searchParam, "name", next.getName());
412                                        terser.addElement(searchParam, "type", next.getParamType().getCode());
413                                        if (isNotBlank(next.getDescription())) {
414                                                terser.addElement(searchParam, "documentation", next.getDescription());
415                                        }
416
417                                        String spUri = next.getUri();
418                                        
419                                        if (isNotBlank(spUri)) {
420                                                terser.addElement(searchParam, "definition", spUri);
421                                        }
422                                }
423
424                                // Add Include to CapabilityStatement.rest.resource
425                                NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName);
426                                if (resourceIncludes.isEmpty()) {
427                                        List<String> includes = searchParams
428                                                .values()
429                                                .stream()
430                                                .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE)
431                                                .map(t -> resourceName + ":" + t.getName())
432                                                .sorted()
433                                                .collect(Collectors.toList());
434                                        terser.addElement(resource, "searchInclude", "*");
435                                        for (String nextInclude : includes) {
436                                                terser.addElement(resource, "searchInclude", nextInclude);
437                                        }
438                                } else {
439                                        for (String resourceInclude : resourceIncludes) {
440                                                terser.addElement(resource, "searchInclude", resourceInclude);
441                                        }
442                                }
443
444                                // Add RevInclude to CapabilityStatement.rest.resource
445                                if (myRestResourceRevIncludesEnabled) {
446                                        NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName);
447                                        if (resourceRevIncludes.isEmpty()) {
448                                                TreeSet<String> revIncludes = new TreeSet<>();
449                                                for (String nextResourceName : resourceToMethods.keySet()) {
450                                                        if (isBlank(nextResourceName)) {
451                                                                continue;
452                                                        }
453
454                                                        for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) {
455                                                                if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
456                                                                        if (isNotBlank(t.getName())) {
457                                                                                boolean appropriateTarget = false;
458                                                                                if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) {
459                                                                                        appropriateTarget = true;
460                                                                                }
461
462                                                                                if (appropriateTarget) {
463                                                                                        revIncludes.add(nextResourceName + ":" + t.getName());
464                                                                                }
465                                                                        }
466                                                                }
467                                                        }
468                                                }
469                                                for (String nextInclude : revIncludes) {
470                                                        terser.addElement(resource, "searchRevInclude", nextInclude);
471                                                }
472                                        } else {
473                                                for (String resourceInclude : resourceRevIncludes) {
474                                                        terser.addElement(resource, "searchRevInclude", resourceInclude);
475                                                }
476                                        }
477                                }
478
479                                // Add SupportedProfile to CapabilityStatement.rest.resource
480                                for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) {
481                                        terser.addElement(resource, "supportedProfile", supportedProfile);
482                                }
483
484                        } else {
485                                for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
486                                        checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding);
487                                        if (nextMethodBinding instanceof OperationMethodBinding) {
488                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
489                                                if (!methodBinding.isGlobalMethod()) {
490                                                        String opName = bindings.getOperationBindingToId().get(methodBinding);
491                                                        if (operationNames.add(opName)) {
492                                                                ourLog.debug("Found bound operation: {}", opName);
493                                                                IBase operation = terser.addElement(rest, "operation");
494                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
495                                                        }
496                                                }
497                                        } else if (nextMethodBinding instanceof SearchMethodBinding) {
498                                                addSearchMethodIfSearchIsNamedQuery(theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding) nextMethodBinding);
499                                        }
500                                }
501                        }
502
503                }
504
505
506                // Find any global operations (Operations defines at the system level but with the
507                // global flag set to true, meaning they apply to all resource types)
508                if (globalMethodBindings != null) {
509                        Set<String> globalOperationNames = new HashSet<>();
510                        for (BaseMethodBinding<?> next : globalMethodBindings) {
511                                if (next instanceof OperationMethodBinding) {
512                                        OperationMethodBinding methodBinding = (OperationMethodBinding) next;
513                                        if (methodBinding.isGlobalMethod()) {
514                                                if (methodBinding.isCanOperateAtServerLevel()) {
515                                                        String opName = bindings.getOperationBindingToId().get(methodBinding);
516                                                        // Only add each operation (by name) once
517                                                        if (globalOperationNames.add(opName)) {
518                                                                IBase operation = terser.addElement(rest, "operation");
519                                                                populateOperation(theRequestDetails, terser, methodBinding, opName, operation);
520                                                        }
521                                                }
522                                        }
523                                }
524                        }
525                }
526
527
528                postProcessRest(terser, rest);
529                postProcess(terser, retVal);
530
531                return retVal;
532        }
533
534        /**
535         *
536         * @param theSearchParam
537         * @return true if theSearchParam is enabled on this server
538         */
539        protected boolean searchParamEnabled(String theSearchParam) {
540                return true;
541        }
542
543        private void addSearchMethodIfSearchIsNamedQuery(RequestDetails theRequestDetails, Bindings theBindings, FhirTerser theTerser, Set<String> theOperationNamesAlreadyAdded, IBase theElementToAddTo, SearchMethodBinding theSearchMethodBinding) {
544                if (theSearchMethodBinding.getQueryName() != null) {
545                        String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding);
546                        if (theOperationNamesAlreadyAdded.add(queryName)) {
547                                IBase operation = theTerser.addElement(theElementToAddTo, "operation");
548                                theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName());
549                                theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName)));
550                        }
551                }
552        }
553
554        private void populateOperation(RequestDetails theRequestDetails, FhirTerser theTerser, OperationMethodBinding theMethodBinding, String theOpName, IBase theOperation) {
555                String operationName = theMethodBinding.getName().substring(1);
556                theTerser.addElement(theOperation, "name", operationName);
557                theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName));
558                if (isNotBlank(theMethodBinding.getDescription())) {
559                        theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription());
560                }
561        }
562
563        @Nonnull
564        private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) {
565                return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName;
566        }
567
568        private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) {
569                TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create();
570                if (myValidationSupport != null) {
571                        List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions();
572                        if (allStructureDefinitions != null) {
573                                for (IBaseResource next : allStructureDefinitions) {
574                                        String kind = terser.getSinglePrimitiveValueOrNull(next, "kind");
575                                        String url = terser.getSinglePrimitiveValueOrNull(next, "url");
576                                        String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition"));
577                                        if ("resource".equals(kind) && isNotBlank(url)) {
578
579                                                // Don't include the base resource definitions in the supported profile list - This isn't helpful
580                                                if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) {
581                                                        continue;
582                                                }
583
584                                                String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path");
585                                                if (isBlank(resourceType)) {
586                                                        resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path");
587                                                }
588
589                                                if (isNotBlank(resourceType)) {
590                                                        resourceTypeToSupportedProfiles.put(resourceType, url);
591                                                }
592                                        }
593                                }
594                        }
595                }
596                return resourceTypeToSupportedProfiles;
597        }
598
599        /**
600         * Subclasses may override
601         */
602        protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) {
603                // nothing
604        }
605
606        /**
607         * Subclasses may override
608         */
609        protected void postProcessRest(FhirTerser theTerser, IBase theRest) {
610                // nothing
611        }
612
613        /**
614         * Subclasses may override
615         */
616        protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) {
617                // nothing
618        }
619
620        protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) {
621                if (theRequestDetails == null) {
622                        return "";
623                }
624                return theRequestDetails.getServerBaseForRequest() + "/";
625        }
626
627
628        @Override
629        @Read(typeName = "OperationDefinition")
630        public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) {
631                if (theId == null || theId.hasIdPart() == false) {
632                        throw new ResourceNotFoundException(Msg.code(1977) + theId);
633                }
634                RestfulServerConfiguration configuration = getServerConfiguration();
635                Bindings bindings = configuration.provideBindings();
636
637                List<OperationMethodBinding> operationBindings = bindings.getOperationIdToBindings().get(theId.getIdPart());
638                if (operationBindings != null && !operationBindings.isEmpty()) {
639                        return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings);
640                }
641
642                List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart());
643                if (searchBindings != null && !searchBindings.isEmpty()) {
644                        return readOperationDefinitionForNamedSearch(searchBindings);
645                }
646                throw new ResourceNotFoundException(Msg.code(1978) + theId);
647        }
648
649        private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) {
650                IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance();
651                FhirTerser terser = myContext.newTerser();
652
653                terser.addElement(op, "status", "active");
654                terser.addElement(op, "kind", "query");
655                terser.addElement(op, "affectsState", "false");
656
657                terser.addElement(op, "instance", "false");
658
659                Set<String> inParams = new HashSet<>();
660
661                String operationCode = null;
662                for (SearchMethodBinding binding : bindings) {
663                        if (isNotBlank(binding.getDescription())) {
664                                terser.addElement(op, "description", binding.getDescription());
665                        }
666                        if (isBlank(binding.getResourceProviderResourceName())) {
667                                terser.addElement(op, "system", "true");
668                                terser.addElement(op, "type", "false");
669                        } else {
670                                terser.addElement(op, "system", "false");
671                                terser.addElement(op, "type", "true");
672                                terser.addElement(op, "resource", binding.getResourceProviderResourceName());
673                        }
674
675                        if (operationCode == null) {
676                                operationCode = binding.getQueryName();
677                        }
678
679                        for (IParameter nextParamUntyped : binding.getParameters()) {
680                                if (nextParamUntyped instanceof SearchParameter) {
681                                        SearchParameter nextParam = (SearchParameter) nextParamUntyped;
682                                        if (!inParams.add(nextParam.getName())) {
683                                                continue;
684                                        }
685
686                                        IBase param = terser.addElement(op, "parameter");
687                                        terser.addElement(param, "use", "in");
688                                        terser.addElement(param, "type", "string");
689                                        terser.addElement(param, "searchType", nextParam.getParamType().getCode());
690                                        terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0");
691                                        terser.addElement(param, "max", "1");
692                                        terser.addElement(param, "name", nextParam.getName());
693                                }
694                        }
695
696                }
697
698                terser.addElement(op, "code", operationCode);
699
700                String operationName = WordUtils.capitalize(operationCode);
701                terser.addElement(op, "name", operationName);
702
703                return op;
704        }
705
706        private IBaseResource readOperationDefinitionForOperation(RequestDetails theRequestDetails, Bindings theBindings, List<OperationMethodBinding> theOperationMethodBindings) {
707                IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance();
708                FhirTerser terser = myContext.newTerser();
709
710                terser.addElement(op, "status", "active");
711                terser.addElement(op, "kind", "operation");
712
713                boolean systemLevel = false;
714                boolean typeLevel = false;
715                boolean instanceLevel = false;
716                boolean affectsState = false;
717                String description = null;
718                String title = null;
719                String code = null;
720                String url = null;
721
722                Set<String> resourceNames = new TreeSet<>();
723                Map<String, IBase> inParams = new HashMap<>();
724                Map<String, IBase> outParams = new HashMap<>();
725
726                for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) {
727                        if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) {
728                                description = operationMethodBinding.getDescription();
729                        }
730                        if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) {
731                                title = operationMethodBinding.getShortDescription();
732                        }
733                        if (operationMethodBinding.isCanOperateAtInstanceLevel()) {
734                                instanceLevel = true;
735                        }
736                        if (operationMethodBinding.isCanOperateAtServerLevel()) {
737                                systemLevel = true;
738                        }
739                        if (operationMethodBinding.isCanOperateAtTypeLevel()) {
740                                typeLevel = true;
741                        }
742                        if (!operationMethodBinding.isIdempotent()) {
743                                affectsState |= true;
744                        }
745
746                        code = operationMethodBinding.getName().substring(1);
747
748                        if (isNotBlank(operationMethodBinding.getResourceName())) {
749                                resourceNames.add(operationMethodBinding.getResourceName());
750                        }
751
752                        if (isBlank(url)) {
753                                url = theBindings.getOperationBindingToId().get(operationMethodBinding);
754                                if (isNotBlank(url)) {
755                                        url = createOperationUrl(theRequestDetails, url);
756                                }
757                        }
758
759
760                        for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) {
761                                if (nextParamUntyped instanceof OperationParameter) {
762                                        OperationParameter nextParam = (OperationParameter) nextParamUntyped;
763
764                                        IBase param = inParams.get(nextParam.getName());
765                                        if (param == null){
766                                                param = terser.addElement(op, "parameter");
767                                                inParams.put(nextParam.getName(), param);
768                                        }
769
770                                        IBase existingParam = inParams.get(nextParam.getName());
771                                        if (isNotBlank(nextParam.getDescription()) && terser.getValues(existingParam, "documentation").isEmpty()) {
772                                                terser.addElement(existingParam, "documentation", nextParam.getDescription());
773                                        }
774
775                                        if (nextParam.getParamType() != null) {
776                                                String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type");
777                                                if (!nextParam.getParamType().equals(existingType)) {
778                                                        if (existingType == null) {
779                                                                terser.setElement(existingParam, "type", nextParam.getParamType());
780                                                        } else {
781                                                                terser.setElement(existingParam, "type", "Resource");
782                                                        }
783                                                }
784                                        }
785
786                                        terser.setElement(param, "use", "in");
787                                        if (nextParam.getSearchParamType() != null) {
788                                                terser.setElement(param, "searchType", nextParam.getSearchParamType());
789                                        }
790                                        terser.setElement(param, "min", Integer.toString(nextParam.getMin()));
791                                        terser.setElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())));
792                                        terser.setElement(param, "name", nextParam.getName());
793
794                                        List<IBaseExtension<?, ?>> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl((IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE);
795                                        Set<String> existingExamples = existingExampleExtensions
796                                                .stream()
797                                                .map(t -> t.getValue())
798                                                .filter(t -> t != null)
799                                                .map(t -> (IPrimitiveType<?>) t)
800                                                .map(t -> t.getValueAsString())
801                                                .collect(Collectors.toSet());
802                                        for (String nextExample : nextParam.getExampleValues()) {
803                                                if (!existingExamples.contains(nextExample)) {
804                                                        ExtensionUtil.addExtension(myContext, param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE, "string", nextExample);
805                                                }
806                                        }
807
808                                }
809                        }
810
811                        for (ReturnType nextParam : operationMethodBinding.getReturnParams()) {
812                                if (outParams.containsKey(nextParam.getName())) {
813                                        continue;
814                                }
815
816                                IBase param = terser.addElement(op, "parameter");
817                                outParams.put(nextParam.getName(), param);
818
819                                terser.addElement(param, "use", "out");
820                                if (nextParam.getType() != null) {
821                                        terser.addElement(param, "type", nextParam.getType());
822                                }
823                                terser.addElement(param, "min", Integer.toString(nextParam.getMin()));
824                                terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())));
825                                terser.addElement(param, "name", nextParam.getName());
826                        }
827                }
828                String name = WordUtils.capitalize(code);
829
830                terser.addElements(op, "resource", resourceNames);
831                terser.addElement(op, "name", name);
832                terser.addElement(op, "url", url);
833                terser.addElement(op, "code", code);
834                terser.addElement(op, "description", description);
835                terser.addElement(op, "title", title);
836                terser.addElement(op, "affectsState", Boolean.toString(affectsState));
837                terser.addElement(op, "system", Boolean.toString(systemLevel));
838                terser.addElement(op, "type", Boolean.toString(typeLevel));
839                terser.addElement(op, "instance", Boolean.toString(instanceLevel));
840
841                return op;
842        }
843
844        @Override
845        public void setRestfulServer(RestfulServer theRestfulServer) {
846                // ignore
847        }
848
849        public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) {
850                myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled;
851        }
852}