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