001package org.hl7.fhir.r4.hapi.rest.server;
002
003import ca.uhn.fhir.context.FhirVersionEnum;
004import ca.uhn.fhir.context.RuntimeResourceDefinition;
005import ca.uhn.fhir.context.RuntimeSearchParam;
006import ca.uhn.fhir.parser.DataFormatException;
007import ca.uhn.fhir.rest.annotation.IdParam;
008import ca.uhn.fhir.rest.annotation.Metadata;
009import ca.uhn.fhir.rest.annotation.Read;
010import ca.uhn.fhir.rest.api.Constants;
011import ca.uhn.fhir.rest.api.server.RequestDetails;
012import ca.uhn.fhir.rest.server.*;
013import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
014import ca.uhn.fhir.rest.server.method.*;
015import ca.uhn.fhir.rest.server.method.SearchParameter;
016import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType;
017import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider;
018import org.apache.commons.lang3.StringUtils;
019import org.hl7.fhir.exceptions.FHIRException;
020import org.hl7.fhir.instance.model.api.IBaseResource;
021import org.hl7.fhir.instance.model.api.IPrimitiveType;
022import org.hl7.fhir.r4.model.*;
023import org.hl7.fhir.r4.model.CapabilityStatement.*;
024import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
025import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
026import org.hl7.fhir.r4.model.OperationDefinition.OperationKind;
027import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse;
028
029import javax.servlet.ServletContext;
030import javax.servlet.http.HttpServletRequest;
031import java.util.*;
032import java.util.Map.Entry;
033
034import static org.apache.commons.lang3.StringUtils.isBlank;
035import static org.apache.commons.lang3.StringUtils.isNotBlank;
036
037/*
038 * #%L
039 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0)
040 * %%
041 * Copyright (C) 2014 - 2015 University Health Network
042 * %%
043 * Licensed under the Apache License, Version 2.0 (the "License");
044 * you may not use this file except in compliance with the License.
045 * You may obtain a copy of the License at
046 *
047 *      http://www.apache.org/licenses/LICENSE-2.0
048 *
049 * Unless required by applicable law or agreed to in writing, software
050 * distributed under the License is distributed on an "AS IS" BASIS,
051 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
052 * See the License for the specific language governing permissions and
053 * limitations under the License.
054 * #L%
055 */
056
057/**
058 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation
059 *
060 * <p>
061 * Note: This class is safe to extend, but it is important to note that the same instance of {@link CapabilityStatement} is always returned unless {@link #setCache(boolean)} is called with a value of
062 * <code>false</code>. This means that if you are adding anything to the returned conformance instance on each call you should call <code>setCache(false)</code> in your provider constructor.
063 * </p>
064 */
065public class ServerCapabilityStatementProvider extends BaseServerCapabilityStatementProvider implements IServerConformanceProvider<CapabilityStatement> {
066
067  private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProvider.class);
068  private String myPublisher = "Not provided";
069
070  /**
071   * No-arg constructor and setter so that the ServerConformanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen.
072   */
073  public ServerCapabilityStatementProvider() {
074    super();
075  }
076
077  /**
078   * Constructor
079   *
080   * @deprecated Use no-args constructor instead. Deprecated in 4.0.0
081   */
082  @Deprecated
083  public ServerCapabilityStatementProvider(RestfulServer theRestfulServer) {
084    this();
085  }
086
087  /**
088   * Constructor - This is intended only for JAX-RS server
089   */
090  public ServerCapabilityStatementProvider(RestfulServerConfiguration theServerConfiguration) {
091    super(theServerConfiguration);
092  }
093
094  private void checkBindingForSystemOps(CapabilityStatementRestComponent rest, Set<SystemRestfulInteraction> systemOps, BaseMethodBinding<?> nextMethodBinding) {
095    if (nextMethodBinding.getRestOperationType() != null) {
096      String sysOpCode = nextMethodBinding.getRestOperationType().getCode();
097      if (sysOpCode != null) {
098        SystemRestfulInteraction sysOp;
099        try {
100          sysOp = SystemRestfulInteraction.fromCode(sysOpCode);
101        } catch (FHIRException e) {
102          return;
103        }
104        if (sysOp == null) {
105          return;
106        }
107        if (systemOps.contains(sysOp) == false) {
108          systemOps.add(sysOp);
109          rest.addInteraction().setCode(sysOp);
110        }
111      }
112    }
113  }
114
115
116
117  private DateTimeType conformanceDate(RequestDetails theRequestDetails) {
118    IPrimitiveType<Date> buildDate = getServerConfiguration(theRequestDetails).getConformanceDate();
119    if (buildDate != null && buildDate.getValue() != null) {
120      try {
121        return new DateTimeType(buildDate.getValueAsString());
122      } catch (DataFormatException e) {
123        // fall through
124      }
125    }
126    return DateTimeType.now();
127  }
128
129
130
131  /**
132   * 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
133   * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
134   */
135  public String getPublisher() {
136    return myPublisher;
137  }
138
139  /**
140   * 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
141   * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
142   */
143  public void setPublisher(String thePublisher) {
144    myPublisher = thePublisher;
145  }
146
147  @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
148  @Override
149  @Metadata
150  public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
151
152    RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails);
153    Bindings bindings = configuration.provideBindings();
154
155    CapabilityStatement retVal = new CapabilityStatement();
156
157    retVal.setPublisher(myPublisher);
158    retVal.setDateElement(conformanceDate(theRequestDetails));
159    retVal.setFhirVersion(Enumerations.FHIRVersion.fromCode(FhirVersionEnum.R4.getFhirVersionString()));
160
161    ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE));
162    String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest);
163    retVal
164      .getImplementation()
165      .setUrl(serverBase)
166      .setDescription(configuration.getImplementationDescription());
167
168    retVal.setKind(CapabilityStatementKind.INSTANCE);
169    retVal.getSoftware().setName(configuration.getServerName());
170    retVal.getSoftware().setVersion(configuration.getServerVersion());
171    retVal.addFormat(Constants.CT_FHIR_XML_NEW);
172    retVal.addFormat(Constants.CT_FHIR_JSON_NEW);
173    retVal.setStatus(PublicationStatus.ACTIVE);
174
175    CapabilityStatementRestComponent rest = retVal.addRest();
176    rest.setMode(RestfulCapabilityMode.SERVER);
177
178    Set<SystemRestfulInteraction> systemOps = new HashSet<>();
179    Set<String> operationNames = new HashSet<>();
180
181    Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings();
182    for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
183
184      if (nextEntry.getKey().isEmpty() == false) {
185        Set<TypeRestfulInteraction> resourceOps = new HashSet<>();
186        CapabilityStatementRestResourceComponent resource = rest.addResource();
187        String resourceName = nextEntry.getKey();
188        RuntimeResourceDefinition def = configuration.getFhirContext().getResourceDefinition(resourceName);
189        resource.getTypeElement().setValue(def.getName());
190        resource.getProfileElement().setValue((def.getResourceProfile(serverBase)));
191
192        TreeSet<String> includes = new TreeSet<>();
193
194        // Map<String, CapabilityStatement.RestResourceSearchParam> nameToSearchParam = new HashMap<String,
195        // CapabilityStatement.RestResourceSearchParam>();
196        for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
197          if (nextMethodBinding.getRestOperationType() != null) {
198            String resOpCode = nextMethodBinding.getRestOperationType().getCode();
199            if (resOpCode != null) {
200              TypeRestfulInteraction resOp;
201              try {
202                resOp = TypeRestfulInteraction.fromCode(resOpCode);
203              } catch (Exception e) {
204                resOp = null;
205              }
206              if (resOp != null) {
207                if (resourceOps.contains(resOp) == false) {
208                  resourceOps.add(resOp);
209                  resource.addInteraction().setCode(resOp);
210                }
211                if ("vread".equals(resOpCode)) {
212                  // vread implies read
213                  resOp = TypeRestfulInteraction.READ;
214                  if (resourceOps.contains(resOp) == false) {
215                    resourceOps.add(resOp);
216                    resource.addInteraction().setCode(resOp);
217                  }
218                }
219
220                if (nextMethodBinding.isSupportsConditional()) {
221                  switch (resOp) {
222                    case CREATE:
223                      resource.setConditionalCreate(true);
224                      break;
225                    case DELETE:
226                      if (nextMethodBinding.isSupportsConditionalMultiple()) {
227                        resource.setConditionalDelete(ConditionalDeleteStatus.MULTIPLE);
228                      } else {
229                        resource.setConditionalDelete(ConditionalDeleteStatus.SINGLE);
230                      }
231                      break;
232                    case UPDATE:
233                      resource.setConditionalUpdate(true);
234                      break;
235                    default:
236                      break;
237                  }
238                }
239              }
240            }
241          }
242
243          checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
244
245          if (nextMethodBinding instanceof SearchMethodBinding) {
246            SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding;
247            if (methodBinding.getQueryName() != null) {
248              String queryName = bindings.getNamedSearchMethodBindingToName().get(methodBinding);
249              if (operationNames.add(queryName)) {
250                rest.addOperation().setName(methodBinding.getQueryName()).setDefinition(("OperationDefinition/" + queryName));
251              }
252            } else {
253              handleNamelessSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails);
254            }
255          } else if (nextMethodBinding instanceof OperationMethodBinding) {
256            OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
257            String opName = bindings.getOperationBindingToName().get(methodBinding);
258            if (operationNames.add(opName)) {
259              // Only add each operation (by name) once
260              rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(("OperationDefinition/" + opName));
261            }
262          }
263
264          resource.getInteraction().sort(new Comparator<ResourceInteractionComponent>() {
265            @Override
266            public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) {
267              TypeRestfulInteraction o1 = theO1.getCode();
268              TypeRestfulInteraction o2 = theO2.getCode();
269              if (o1 == null && o2 == null) {
270                return 0;
271              }
272              if (o1 == null) {
273                return 1;
274              }
275              if (o2 == null) {
276                return -1;
277              }
278              return o1.ordinal() - o2.ordinal();
279            }
280          });
281
282        }
283
284        for (String nextInclude : includes) {
285          resource.addSearchInclude(nextInclude);
286        }
287      } else {
288        for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
289          checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
290          if (nextMethodBinding instanceof OperationMethodBinding) {
291            OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
292            String opName = bindings.getOperationBindingToName().get(methodBinding);
293            if (operationNames.add(opName)) {
294              ourLog.debug("Found bound operation: {}", opName);
295              rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(("OperationDefinition/" + opName));
296            }
297          }
298        }
299      }
300    }
301
302    return retVal;
303  }
304
305  private void handleNamelessSearchMethodBinding(CapabilityStatementRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet<String> includes,
306                                                 SearchMethodBinding searchMethodBinding, RequestDetails theRequestDetails) {
307    includes.addAll(searchMethodBinding.getIncludes());
308
309    List<IParameter> params = searchMethodBinding.getParameters();
310    List<SearchParameter> searchParameters = new ArrayList<>();
311    for (IParameter nextParameter : params) {
312      if ((nextParameter instanceof SearchParameter)) {
313        searchParameters.add((SearchParameter) nextParameter);
314      }
315    }
316    sortSearchParameters(searchParameters);
317    if (!searchParameters.isEmpty()) {
318      // boolean allOptional = searchParameters.get(0).isRequired() == false;
319      //
320      // OperationDefinition query = null;
321      // if (!allOptional) {
322      // RestOperation operation = rest.addOperation();
323      // query = new OperationDefinition();
324      // operation.setDefinition(new ResourceReferenceDt(query));
325      // query.getDescriptionElement().setValue(searchMethodBinding.getDescription());
326      // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName));
327      // for (String nextInclude : searchMethodBinding.getIncludes()) {
328      // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude));
329      // }
330      // }
331
332      for (SearchParameter nextParameter : searchParameters) {
333
334        String nextParamName = nextParameter.getName();
335
336        String chain = null;
337        String nextParamUnchainedName = nextParamName;
338        if (nextParamName.contains(".")) {
339          chain = nextParamName.substring(nextParamName.indexOf('.') + 1);
340          nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
341        }
342
343        String nextParamDescription = nextParameter.getDescription();
344
345        /*
346         * If the parameter has no description, default to the one from the resource
347         */
348        if (StringUtils.isBlank(nextParamDescription)) {
349          RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
350          if (paramDef != null) {
351            nextParamDescription = paramDef.getDescription();
352          }
353        }
354
355        CapabilityStatementRestResourceSearchParamComponent param = resource.addSearchParam();
356        param.setName(nextParamUnchainedName);
357
358//                              if (StringUtils.isNotBlank(chain)) {
359//                                      param.addChain(chain);
360//                              }
361//
362//                              if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
363//                                      for (String nextWhitelist : new TreeSet<String>(nextParameter.getQualifierWhitelist())) {
364//                                              if (nextWhitelist.startsWith(".")) {
365//                                                      param.addChain(nextWhitelist.substring(1));
366//                                              }
367//                                      }
368//                              }
369
370        param.setDocumentation(nextParamDescription);
371        if (nextParameter.getParamType() != null) {
372          param.getTypeElement().setValueAsString(nextParameter.getParamType().getCode());
373        }
374        for (Class<? extends IBaseResource> nextTarget : nextParameter.getDeclaredTypes()) {
375          RuntimeResourceDefinition targetDef = getServerConfiguration(theRequestDetails).getFhirContext().getResourceDefinition(nextTarget);
376          if (targetDef != null) {
377            ResourceType code;
378            try {
379              code = ResourceType.fromCode(targetDef.getName());
380            } catch (FHIRException e) {
381              code = null;
382            }
383//                                              if (code != null) {
384//                                                      param.addTarget(targetDef.getName());
385//                                              }
386          }
387        }
388      }
389    }
390  }
391
392
393
394  @Read(type = OperationDefinition.class)
395  public OperationDefinition readOperationDefinition(@IdParam IdType theId, RequestDetails theRequestDetails) {
396    if (theId == null || theId.hasIdPart() == false) {
397      throw new ResourceNotFoundException(theId);
398    }
399    RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails);
400    Bindings bindings = configuration.provideBindings();
401
402    List<OperationMethodBinding> operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart());
403    if (operationBindings != null && !operationBindings.isEmpty()) {
404      return readOperationDefinitionForOperation(operationBindings);
405    }
406    List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart());
407    if (searchBindings != null && !searchBindings.isEmpty()) {
408      return readOperationDefinitionForNamedSearch(searchBindings);
409    }
410    throw new ResourceNotFoundException(theId);
411  }
412
413  private OperationDefinition readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) {
414    OperationDefinition op = new OperationDefinition();
415    op.setStatus(PublicationStatus.ACTIVE);
416    op.setKind(OperationKind.QUERY);
417    op.setAffectsState(false);
418
419    op.setSystem(false);
420    op.setType(false);
421    op.setInstance(false);
422
423    Set<String> inParams = new HashSet<>();
424
425    for (SearchMethodBinding binding : bindings) {
426      if (isNotBlank(binding.getDescription())) {
427        op.setDescription(binding.getDescription());
428      }
429      if (isBlank(binding.getResourceProviderResourceName())) {
430        op.setSystem(true);
431      } else {
432        op.setType(true);
433        op.addResourceElement().setValue(binding.getResourceProviderResourceName());
434      }
435      op.setCode(binding.getQueryName());
436      for (IParameter nextParamUntyped : binding.getParameters()) {
437        if (nextParamUntyped instanceof SearchParameter) {
438          SearchParameter nextParam = (SearchParameter) nextParamUntyped;
439          if (!inParams.add(nextParam.getName())) {
440            continue;
441          }
442          OperationDefinitionParameterComponent param = op.addParameter();
443          param.setUse(OperationParameterUse.IN);
444          param.setType("string");
445          param.getSearchTypeElement().setValueAsString(nextParam.getParamType().getCode());
446          param.setMin(nextParam.isRequired() ? 1 : 0);
447          param.setMax("1");
448          param.setName(nextParam.getName());
449        }
450      }
451
452      if (isBlank(op.getName())) {
453        if (isNotBlank(op.getDescription())) {
454          op.setName(op.getDescription());
455        } else {
456          op.setName(op.getCode());
457        }
458      }
459    }
460
461    return op;
462  }
463
464  private OperationDefinition readOperationDefinitionForOperation(List<OperationMethodBinding> bindings) {
465    OperationDefinition op = new OperationDefinition();
466    op.setStatus(PublicationStatus.ACTIVE);
467    op.setKind(OperationKind.OPERATION);
468    op.setAffectsState(false);
469
470    // We reset these to true below if we find a binding that can handle the level
471    op.setSystem(false);
472    op.setType(false);
473    op.setInstance(false);
474
475    Set<String> inParams = new HashSet<>();
476    Set<String> outParams = new HashSet<>();
477
478    for (OperationMethodBinding sharedDescription : bindings) {
479      if (isNotBlank(sharedDescription.getDescription())) {
480        op.setDescription(sharedDescription.getDescription());
481      }
482      if (sharedDescription.isCanOperateAtInstanceLevel()) {
483        op.setInstance(true);
484      }
485      if (sharedDescription.isCanOperateAtServerLevel()) {
486        op.setSystem(true);
487      }
488      if (sharedDescription.isCanOperateAtTypeLevel()) {
489        op.setType(true);
490      }
491      if (!sharedDescription.isIdempotent()) {
492        op.setAffectsState(!sharedDescription.isIdempotent());
493      }
494      op.setCode(sharedDescription.getName().substring(1));
495      if (sharedDescription.isCanOperateAtInstanceLevel()) {
496        op.setInstance(sharedDescription.isCanOperateAtInstanceLevel());
497      }
498      if (sharedDescription.isCanOperateAtServerLevel()) {
499        op.setSystem(sharedDescription.isCanOperateAtServerLevel());
500      }
501      if (isNotBlank(sharedDescription.getResourceName())) {
502        op.addResourceElement().setValue(sharedDescription.getResourceName());
503      }
504
505      for (IParameter nextParamUntyped : sharedDescription.getParameters()) {
506        if (nextParamUntyped instanceof OperationParameter) {
507          OperationParameter nextParam = (OperationParameter) nextParamUntyped;
508          OperationDefinitionParameterComponent param = op.addParameter();
509          if (!inParams.add(nextParam.getName())) {
510            continue;
511          }
512          param.setUse(OperationParameterUse.IN);
513          if (nextParam.getParamType() != null) {
514            param.setType(nextParam.getParamType());
515          }
516          if (nextParam.getSearchParamType() != null) {
517            param.getSearchTypeElement().setValueAsString(nextParam.getSearchParamType());
518          }
519          param.setMin(nextParam.getMin());
520          param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
521          param.setName(nextParam.getName());
522        }
523      }
524
525      for (ReturnType nextParam : sharedDescription.getReturnParams()) {
526        if (!outParams.add(nextParam.getName())) {
527          continue;
528        }
529        OperationDefinitionParameterComponent param = op.addParameter();
530        param.setUse(OperationParameterUse.OUT);
531        if (nextParam.getType() != null) {
532          param.setType(nextParam.getType());
533        }
534        param.setMin(nextParam.getMin());
535        param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
536        param.setName(nextParam.getName());
537      }
538    }
539
540    if (isBlank(op.getName())) {
541      if (isNotBlank(op.getDescription())) {
542        op.setName(op.getDescription());
543      } else {
544        op.setName(op.getCode());
545      }
546    }
547
548    if (op.hasSystem() == false) {
549      op.setSystem(false);
550    }
551    if (op.hasInstance() == false) {
552      op.setInstance(false);
553    }
554
555    return op;
556  }
557
558  /**
559   * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation.
560   * <p>
561   * See the class documentation for an important note if you are extending this class
562   * </p>
563   *
564   * @deprecated Since 4.0.0 - This method no longer does anything
565   */
566  @Deprecated
567  public ServerCapabilityStatementProvider setCache(boolean theCache) {
568    return this;
569  }
570
571  @Override
572  public void setRestfulServer(RestfulServer theRestfulServer) {
573    // ignore
574  }
575
576  private void sortRuntimeSearchParameters(List<RuntimeSearchParam> searchParameters) {
577    Collections.sort(searchParameters, new Comparator<RuntimeSearchParam>() {
578      @Override
579      public int compare(RuntimeSearchParam theO1, RuntimeSearchParam theO2) {
580        return theO1.getName().compareTo(theO2.getName());
581      }
582    });
583  }
584
585  private void sortSearchParameters(List<SearchParameter> searchParameters) {
586    Collections.sort(searchParameters, new Comparator<SearchParameter>() {
587      @Override
588      public int compare(SearchParameter theO1, SearchParameter theO2) {
589        if (theO1.isRequired() == theO2.isRequired()) {
590          return theO1.getName().compareTo(theO2.getName());
591        }
592        if (theO1.isRequired()) {
593          return -1;
594        }
595        return 1;
596      }
597    });
598  }
599}