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