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}