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