001/*
002 * #%L
003 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0)
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.dstu2;
021
022import ca.uhn.fhir.context.FhirVersionEnum;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.model.dstu2.resource.Conformance;
027import ca.uhn.fhir.model.dstu2.resource.Conformance.Rest;
028import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource;
029import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResourceInteraction;
030import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResourceSearchParam;
031import ca.uhn.fhir.model.dstu2.resource.OperationDefinition;
032import ca.uhn.fhir.model.dstu2.resource.OperationDefinition.Parameter;
033import ca.uhn.fhir.model.dstu2.valueset.*;
034import ca.uhn.fhir.model.primitive.DateTimeDt;
035import ca.uhn.fhir.model.primitive.IdDt;
036import ca.uhn.fhir.parser.DataFormatException;
037import ca.uhn.fhir.rest.annotation.IdParam;
038import ca.uhn.fhir.rest.annotation.Metadata;
039import ca.uhn.fhir.rest.annotation.Read;
040import ca.uhn.fhir.rest.api.Constants;
041import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
042import ca.uhn.fhir.rest.api.server.RequestDetails;
043import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
044import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType;
045import ca.uhn.fhir.rest.server.*;
046import ca.uhn.fhir.rest.server.method.*;
047import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider;
048import jakarta.servlet.ServletContext;
049import jakarta.servlet.http.HttpServletRequest;
050import org.apache.commons.lang3.StringUtils;
051import org.hl7.fhir.instance.model.api.IBaseResource;
052import org.hl7.fhir.instance.model.api.IPrimitiveType;
053
054import java.util.Map.Entry;
055import java.util.*;
056
057import static org.apache.commons.lang3.StringUtils.isBlank;
058import static org.apache.commons.lang3.StringUtils.isNotBlank;
059
060/**
061 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation
062 */
063public class ServerConformanceProvider extends BaseServerCapabilityStatementProvider
064                implements IServerConformanceProvider<Conformance> {
065
066        private String myPublisher = "Not provided";
067
068        /**
069         * No-arg constructor and setter so that the ServerConfirmanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen.
070         */
071        public ServerConformanceProvider() {
072                super();
073        }
074
075        /**
076         * Constructor
077         *
078         * @deprecated Use no-args constructor instead. Deprecated in 4.0.0
079         */
080        @Deprecated
081        public ServerConformanceProvider(RestfulServer theRestfulServer) {
082                this();
083        }
084
085        /**
086         * Constructor
087         */
088        public ServerConformanceProvider(RestfulServerConfiguration theServerConfiguration) {
089                super(theServerConfiguration);
090        }
091
092        private void checkBindingForSystemOps(
093                        Rest rest, Set<SystemRestfulInteractionEnum> systemOps, BaseMethodBinding nextMethodBinding) {
094                if (nextMethodBinding.getRestOperationType() != null) {
095                        String sysOpCode = nextMethodBinding.getRestOperationType().getCode();
096                        if (sysOpCode != null) {
097                                SystemRestfulInteractionEnum sysOp =
098                                                SystemRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(sysOpCode);
099                                if (sysOp == null) {
100                                        return;
101                                }
102                                if (systemOps.contains(sysOp) == false) {
103                                        systemOps.add(sysOp);
104                                        rest.addInteraction().setCode(sysOp);
105                                }
106                        }
107                }
108        }
109
110        private Map<String, List<BaseMethodBinding>> collectMethodBindings(RequestDetails theRequestDetails) {
111                Map<String, List<BaseMethodBinding>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding>>();
112                for (ResourceBinding next : getServerConfiguration(theRequestDetails).getResourceBindings()) {
113                        String resourceName = next.getResourceName();
114                        for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) {
115                                if (resourceToMethods.containsKey(resourceName) == false) {
116                                        resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding>());
117                                }
118                                resourceToMethods.get(resourceName).add(nextMethodBinding);
119                        }
120                }
121                for (BaseMethodBinding nextMethodBinding :
122                                getServerConfiguration(theRequestDetails).getServerBindings()) {
123                        String resourceName = "";
124                        if (resourceToMethods.containsKey(resourceName) == false) {
125                                resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding>());
126                        }
127                        resourceToMethods.get(resourceName).add(nextMethodBinding);
128                }
129                return resourceToMethods;
130        }
131
132        private DateTimeDt conformanceDate(RequestDetails theRequestDetails) {
133                IPrimitiveType<Date> buildDate =
134                                getServerConfiguration(theRequestDetails).getConformanceDate();
135                if (buildDate != null && buildDate.getValue() != null) {
136                        try {
137                                return new DateTimeDt(buildDate.getValueAsString());
138                        } catch (DataFormatException e) {
139                                // fall through
140                        }
141                }
142                return DateTimeDt.withCurrentTime();
143        }
144
145        private String createOperationName(OperationMethodBinding theMethodBinding) {
146                StringBuilder retVal = new StringBuilder();
147                if (theMethodBinding.getResourceName() != null) {
148                        retVal.append(theMethodBinding.getResourceName());
149                }
150
151                retVal.append('-');
152                if (theMethodBinding.isCanOperateAtInstanceLevel()) {
153                        retVal.append('i');
154                }
155                if (theMethodBinding.isCanOperateAtServerLevel()) {
156                        retVal.append('s');
157                }
158                retVal.append('-');
159
160                // Exclude the leading $
161                retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length());
162
163                return retVal.toString();
164        }
165
166        /**
167         * 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
168         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
169         */
170        public String getPublisher() {
171                return myPublisher;
172        }
173
174        /**
175         * 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
176         * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
177         */
178        public void setPublisher(String thePublisher) {
179                myPublisher = thePublisher;
180        }
181
182        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
183        @Override
184        @Metadata
185        public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
186                RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails);
187                Bindings bindings = serverConfiguration.provideBindings();
188
189                Conformance retVal = new Conformance();
190
191                retVal.setPublisher(myPublisher);
192                retVal.setDate(conformanceDate(theRequestDetails));
193                retVal.setFhirVersion(FhirVersionEnum.DSTU2.getFhirVersionString());
194                retVal.setAcceptUnknown(
195                                UnknownContentCodeEnum
196                                                .UNKNOWN_EXTENSIONS); // TODO: make this configurable - this is a fairly big effort since the
197                // parser
198                // needs to be modified to actually allow it
199
200                ServletContext servletContext = (ServletContext)
201                                (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE));
202                String serverBase =
203                                serverConfiguration.getServerAddressStrategy().determineServerBase(servletContext, theRequest);
204                retVal.getImplementation()
205                                .setUrl(serverBase)
206                                .setDescription(serverConfiguration.getImplementationDescription());
207
208                retVal.setKind(ConformanceStatementKindEnum.INSTANCE);
209                retVal.getSoftware().setName(serverConfiguration.getServerName());
210                retVal.getSoftware().setVersion(serverConfiguration.getServerVersion());
211                retVal.addFormat(Constants.CT_FHIR_XML);
212                retVal.addFormat(Constants.CT_FHIR_JSON);
213
214                Rest rest = retVal.addRest();
215                rest.setMode(RestfulConformanceModeEnum.SERVER);
216
217                Set<SystemRestfulInteractionEnum> systemOps = new HashSet<>();
218                Set<String> operationNames = new HashSet<>();
219
220                Map<String, List<BaseMethodBinding>> resourceToMethods = collectMethodBindings(theRequestDetails);
221                for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) {
222
223                        if (nextEntry.getKey().isEmpty() == false) {
224                                Set<TypeRestfulInteractionEnum> resourceOps = new HashSet<>();
225                                RestResource resource = rest.addResource();
226                                String resourceName = nextEntry.getKey();
227                                RuntimeResourceDefinition def =
228                                                serverConfiguration.getFhirContext().getResourceDefinition(resourceName);
229                                resource.getTypeElement().setValue(def.getName());
230                                resource.getProfile().setReference(new IdDt(def.getResourceProfile(serverBase)));
231
232                                TreeSet<String> includes = new TreeSet<>();
233
234                                // Map<String, Conformance.RestResourceSearchParam> nameToSearchParam = new HashMap<String,
235                                // Conformance.RestResourceSearchParam>();
236                                for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) {
237                                        if (nextMethodBinding.getRestOperationType() != null) {
238                                                String resOpCode =
239                                                                nextMethodBinding.getRestOperationType().getCode();
240                                                if (resOpCode != null) {
241                                                        TypeRestfulInteractionEnum resOp =
242                                                                        TypeRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(resOpCode);
243                                                        if (resOp != null) {
244                                                                if (resourceOps.contains(resOp) == false) {
245                                                                        resourceOps.add(resOp);
246                                                                        resource.addInteraction().setCode(resOp);
247                                                                }
248                                                                if ("vread".equals(resOpCode)) {
249                                                                        // vread implies read
250                                                                        resOp = TypeRestfulInteractionEnum.READ;
251                                                                        if (resourceOps.contains(resOp) == false) {
252                                                                                resourceOps.add(resOp);
253                                                                                resource.addInteraction().setCode(resOp);
254                                                                        }
255                                                                }
256
257                                                                if (nextMethodBinding.isSupportsConditional()) {
258                                                                        switch (resOp) {
259                                                                                case CREATE:
260                                                                                        resource.setConditionalCreate(true);
261                                                                                        break;
262                                                                                case DELETE:
263                                                                                        if (nextMethodBinding.isSupportsConditionalMultiple()) {
264                                                                                                resource.setConditionalDelete(
265                                                                                                                ConditionalDeleteStatusEnum.MULTIPLE_DELETES_SUPPORTED);
266                                                                                        } else {
267                                                                                                resource.setConditionalDelete(
268                                                                                                                ConditionalDeleteStatusEnum.SINGLE_DELETES_SUPPORTED);
269                                                                                        }
270                                                                                        break;
271                                                                                case UPDATE:
272                                                                                        resource.setConditionalUpdate(true);
273                                                                                        break;
274                                                                                default:
275                                                                                        break;
276                                                                        }
277                                                                }
278                                                        }
279                                                }
280                                        }
281
282                                        checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
283
284                                        if (nextMethodBinding instanceof SearchMethodBinding) {
285                                                handleSearchMethodBinding(
286                                                                resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails);
287                                        } else if (nextMethodBinding instanceof OperationMethodBinding) {
288                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
289                                                String opName = bindings.getOperationBindingToId().get(methodBinding);
290                                                if (operationNames.add(opName)) {
291                                                        // Only add each operation (by name) once
292                                                        rest.addOperation()
293                                                                        .setName(methodBinding.getName().substring(1))
294                                                                        .getDefinition()
295                                                                        .setReference("OperationDefinition/" + opName);
296                                                }
297                                        }
298
299                                        Collections.sort(resource.getInteraction(), new Comparator<RestResourceInteraction>() {
300                                                @Override
301                                                public int compare(RestResourceInteraction theO1, RestResourceInteraction theO2) {
302                                                        TypeRestfulInteractionEnum o1 =
303                                                                        theO1.getCodeElement().getValueAsEnum();
304                                                        TypeRestfulInteractionEnum o2 =
305                                                                        theO2.getCodeElement().getValueAsEnum();
306                                                        if (o1 == null && o2 == null) {
307                                                                return 0;
308                                                        }
309                                                        if (o1 == null) {
310                                                                return 1;
311                                                        }
312                                                        if (o2 == null) {
313                                                                return -1;
314                                                        }
315                                                        return o1.ordinal() - o2.ordinal();
316                                                }
317                                        });
318                                }
319
320                                for (String nextInclude : includes) {
321                                        resource.addSearchInclude(nextInclude);
322                                }
323                        } else {
324                                for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) {
325                                        checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
326                                        if (nextMethodBinding instanceof OperationMethodBinding) {
327                                                OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
328                                                String opName = bindings.getOperationBindingToId().get(methodBinding);
329                                                if (operationNames.add(opName)) {
330                                                        rest.addOperation()
331                                                                        .setName(methodBinding.getName().substring(1))
332                                                                        .getDefinition()
333                                                                        .setReference("OperationDefinition/" + opName);
334                                                }
335                                        }
336                                }
337                        }
338                }
339
340                return retVal;
341        }
342
343        private void handleSearchMethodBinding(
344                        RestResource resource,
345                        RuntimeResourceDefinition def,
346                        TreeSet<String> includes,
347                        SearchMethodBinding searchMethodBinding,
348                        RequestDetails theRequestDetails) {
349                includes.addAll(searchMethodBinding.getIncludes());
350
351                List<IParameter> params = searchMethodBinding.getParameters();
352                List<SearchParameter> searchParameters = new ArrayList<>();
353                for (IParameter nextParameter : params) {
354                        if ((nextParameter instanceof SearchParameter)) {
355                                searchParameters.add((SearchParameter) nextParameter);
356                        }
357                }
358                sortSearchParameters(searchParameters);
359                if (!searchParameters.isEmpty()) {
360                        // boolean allOptional = searchParameters.get(0).isRequired() == false;
361                        //
362                        // OperationDefinition query = null;
363                        // if (!allOptional) {
364                        // RestOperation operation = rest.addOperation();
365                        // query = new OperationDefinition();
366                        // operation.setDefinition(new ResourceReferenceDt(query));
367                        // query.getDescriptionElement().setValue(searchMethodBinding.getDescription());
368                        // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName));
369                        // for (String nextInclude : searchMethodBinding.getIncludes()) {
370                        // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude));
371                        // }
372                        // }
373
374                        for (SearchParameter nextParameter : searchParameters) {
375
376                                String nextParamName = nextParameter.getName();
377
378                                String chain = null;
379                                String nextParamUnchainedName = nextParamName;
380                                if (nextParamName.contains(".")) {
381                                        chain = nextParamName.substring(nextParamName.indexOf('.') + 1);
382                                        nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
383                                }
384
385                                String nextParamDescription = nextParameter.getDescription();
386
387                                /*
388                                 * If the parameter has no description, default to the one from the resource
389                                 */
390                                if (StringUtils.isBlank(nextParamDescription)) {
391                                        RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
392                                        if (paramDef != null) {
393                                                nextParamDescription = paramDef.getDescription();
394                                        }
395                                }
396
397                                String finalNextParamUnchainedName = nextParamUnchainedName;
398                                RestResourceSearchParam param = resource.getSearchParam().stream()
399                                                .filter(t -> t.getName().equals(finalNextParamUnchainedName))
400                                                .findFirst()
401                                                .orElseGet(() -> resource.addSearchParam());
402
403                                param.setName(nextParamUnchainedName);
404                                if (StringUtils.isNotBlank(chain)) {
405                                        param.addChain(chain);
406                                } else {
407                                        if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
408                                                for (String nextWhitelist : new TreeSet<>(nextParameter.getQualifierWhitelist())) {
409                                                        if (nextWhitelist.startsWith(".")) {
410                                                                param.addChain(nextWhitelist.substring(1));
411                                                        }
412                                                }
413                                        }
414                                }
415
416                                param.setDocumentation(nextParamDescription);
417                                if (nextParameter.getParamType() != null) {
418                                        param.getTypeElement()
419                                                        .setValueAsString(nextParameter.getParamType().getCode());
420                                }
421                                for (Class<? extends IBaseResource> nextTarget : nextParameter.getDeclaredTypes()) {
422                                        RuntimeResourceDefinition targetDef = getServerConfiguration(theRequestDetails)
423                                                        .getFhirContext()
424                                                        .getResourceDefinition(nextTarget);
425                                        if (targetDef != null) {
426                                                ResourceTypeEnum code = ResourceTypeEnum.VALUESET_BINDER.fromCodeString(targetDef.getName());
427                                                if (code != null) {
428                                                        param.addTarget(code);
429                                                }
430                                        }
431                                }
432                        }
433                }
434        }
435
436        @Read(type = OperationDefinition.class)
437        public OperationDefinition readOperationDefinition(@IdParam IdDt theId, RequestDetails theRequestDetails) {
438                if (theId == null || theId.hasIdPart() == false) {
439                        throw new ResourceNotFoundException(Msg.code(1988) + theId);
440                }
441                RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails);
442                Bindings bindings = serverConfiguration.provideBindings();
443
444                List<OperationMethodBinding> sharedDescriptions =
445                                bindings.getOperationIdToBindings().get(theId.getIdPart());
446                if (sharedDescriptions == null || sharedDescriptions.isEmpty()) {
447                        throw new ResourceNotFoundException(Msg.code(1989) + theId);
448                }
449
450                OperationDefinition op = new OperationDefinition();
451                op.setStatus(ConformanceResourceStatusEnum.ACTIVE);
452                op.setKind(OperationKindEnum.OPERATION);
453                op.setIdempotent(true);
454
455                Set<String> inParams = new HashSet<>();
456                Set<String> outParams = new HashSet<>();
457
458                for (OperationMethodBinding sharedDescription : sharedDescriptions) {
459                        if (isNotBlank(sharedDescription.getDescription())) {
460                                op.setDescription(sharedDescription.getDescription());
461                        }
462                        if (!sharedDescription.isIdempotent()) {
463                                op.setIdempotent(sharedDescription.isIdempotent());
464                        }
465                        op.setCode(sharedDescription.getName().substring(1));
466                        if (sharedDescription.isCanOperateAtInstanceLevel()) {
467                                op.setInstance(sharedDescription.isCanOperateAtInstanceLevel());
468                        }
469                        if (sharedDescription.isCanOperateAtServerLevel()) {
470                                op.setSystem(sharedDescription.isCanOperateAtServerLevel());
471                        }
472                        if (isNotBlank(sharedDescription.getResourceName())) {
473                                op.addType().setValue(sharedDescription.getResourceName());
474                        }
475
476                        for (IParameter nextParamUntyped : sharedDescription.getParameters()) {
477                                if (nextParamUntyped instanceof OperationParameter) {
478                                        OperationParameter nextParam = (OperationParameter) nextParamUntyped;
479                                        if (!inParams.add(nextParam.getName())) {
480                                                continue;
481                                        }
482                                        Parameter param = op.addParameter();
483                                        param.setUse(OperationParameterUseEnum.IN);
484                                        if (nextParam.getParamType() != null) {
485                                                param.setType(nextParam.getParamType());
486                                        }
487                                        param.setMin(nextParam.getMin());
488                                        param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
489                                        param.setName(nextParam.getName());
490                                }
491                        }
492
493                        for (ReturnType nextParam : sharedDescription.getReturnParams()) {
494                                if (!outParams.add(nextParam.getName())) {
495                                        continue;
496                                }
497                                Parameter param = op.addParameter();
498                                param.setUse(OperationParameterUseEnum.OUT);
499                                if (nextParam.getType() != null) {
500                                        param.setType(nextParam.getType());
501                                }
502                                param.setMin(nextParam.getMin());
503                                param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
504                                param.setName(nextParam.getName());
505                        }
506                }
507
508                if (isBlank(op.getName())) {
509                        if (isNotBlank(op.getDescription())) {
510                                op.setName(op.getDescription());
511                        } else {
512                                op.setName(op.getCode());
513                        }
514                }
515
516                if (op.getSystem() == null) {
517                        op.setSystem(false);
518                }
519                if (op.getInstance() == null) {
520                        op.setInstance(false);
521                }
522
523                return op;
524        }
525
526        /**
527         * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation.
528         * <p>
529         * See the class documentation for an important note if you are extending this class
530         * </p>
531         * @deprecated Since 4.0.0 this does nothing
532         */
533        @Deprecated
534        public void setCache(boolean theCache) {
535                // nothing
536        }
537
538        @Override
539        public void setRestfulServer(RestfulServer theRestfulServer) {
540                // nothing
541        }
542
543        private void sortSearchParameters(List<SearchParameter> searchParameters) {
544                Collections.sort(searchParameters, new Comparator<SearchParameter>() {
545                        @Override
546                        public int compare(SearchParameter theO1, SearchParameter theO2) {
547                                if (theO1.isRequired() == theO2.isRequired()) {
548                                        return theO1.getName().compareTo(theO2.getName());
549                                }
550                                if (theO1.isRequired()) {
551                                        return -1;
552                                }
553                                return 1;
554                        }
555                });
556        }
557}