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