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