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