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