001package ca.uhn.fhir.interceptor.executor; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IBaseInterceptorBroadcaster; 026import ca.uhn.fhir.interceptor.api.IBaseInterceptorService; 027import ca.uhn.fhir.interceptor.api.IPointcut; 028import ca.uhn.fhir.interceptor.api.Interceptor; 029import ca.uhn.fhir.interceptor.api.Pointcut; 030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 031import ca.uhn.fhir.util.ReflectionUtil; 032import com.google.common.annotations.VisibleForTesting; 033import com.google.common.collect.ArrayListMultimap; 034import com.google.common.collect.ListMultimap; 035import com.google.common.collect.Multimaps; 036import org.apache.commons.lang3.Validate; 037import org.apache.commons.lang3.builder.ToStringBuilder; 038import org.apache.commons.lang3.builder.ToStringStyle; 039import org.apache.commons.lang3.reflect.MethodUtils; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043import javax.annotation.Nonnull; 044import javax.annotation.Nullable; 045import java.lang.annotation.Annotation; 046import java.lang.reflect.AnnotatedElement; 047import java.lang.reflect.InvocationTargetException; 048import java.lang.reflect.Method; 049import java.util.ArrayList; 050import java.util.Arrays; 051import java.util.Collection; 052import java.util.Collections; 053import java.util.Comparator; 054import java.util.HashMap; 055import java.util.IdentityHashMap; 056import java.util.List; 057import java.util.Map; 058import java.util.Optional; 059import java.util.concurrent.atomic.AtomicInteger; 060import java.util.function.Predicate; 061import java.util.stream.Collectors; 062 063public abstract class BaseInterceptorService<POINTCUT extends IPointcut> implements IBaseInterceptorService<POINTCUT>, IBaseInterceptorBroadcaster<POINTCUT> { 064 private static final Logger ourLog = LoggerFactory.getLogger(BaseInterceptorService.class); 065 private final List<Object> myInterceptors = new ArrayList<>(); 066 private final ListMultimap<POINTCUT, BaseInvoker> myGlobalInvokers = ArrayListMultimap.create(); 067 private final ListMultimap<POINTCUT, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create(); 068 private final Object myRegistryMutex = new Object(); 069 private final ThreadLocal<ListMultimap<POINTCUT, BaseInvoker>> myThreadlocalInvokers = new ThreadLocal<>(); 070 private String myName; 071 private boolean myThreadlocalInvokersEnabled = true; 072 private boolean myWarnOnInterceptorWithNoHooks = true; 073 074 /** 075 * Constructor which uses a default name of "default" 076 */ 077 public BaseInterceptorService() { 078 this("default"); 079 } 080 081 /** 082 * Constructor 083 * 084 * @param theName The name for this registry (useful for troubleshooting) 085 */ 086 public BaseInterceptorService(String theName) { 087 super(); 088 myName = theName; 089 } 090 091 /** 092 * Should a warning be issued if an interceptor is registered and it has no hooks 093 */ 094 public void setWarnOnInterceptorWithNoHooks(boolean theWarnOnInterceptorWithNoHooks) { 095 myWarnOnInterceptorWithNoHooks = theWarnOnInterceptorWithNoHooks; 096 } 097 098 /** 099 * Are threadlocal interceptors enabled on this registry (defaults to true) 100 */ 101 public boolean isThreadlocalInvokersEnabled() { 102 return myThreadlocalInvokersEnabled; 103 } 104 105 /** 106 * Are threadlocal interceptors enabled on this registry (defaults to true) 107 */ 108 public void setThreadlocalInvokersEnabled(boolean theThreadlocalInvokersEnabled) { 109 myThreadlocalInvokersEnabled = theThreadlocalInvokersEnabled; 110 } 111 112 @VisibleForTesting 113 List<Object> getGlobalInterceptorsForUnitTest() { 114 return myInterceptors; 115 } 116 117 public void setName(String theName) { 118 myName = theName; 119 } 120 121 protected void registerAnonymousInterceptor(POINTCUT thePointcut, Object theInterceptor, BaseInvoker theInvoker) { 122 Validate.notNull(thePointcut); 123 Validate.notNull(theInterceptor); 124 synchronized (myRegistryMutex) { 125 126 myAnonymousInvokers.put(thePointcut, theInvoker); 127 if (!isInterceptorAlreadyRegistered(theInterceptor)) { 128 myInterceptors.add(theInterceptor); 129 } 130 } 131 } 132 133 @Override 134 public List<Object> getAllRegisteredInterceptors() { 135 synchronized (myRegistryMutex) { 136 List<Object> retVal = new ArrayList<>(); 137 retVal.addAll(myInterceptors); 138 return Collections.unmodifiableList(retVal); 139 } 140 } 141 142 @Override 143 @VisibleForTesting 144 public void unregisterAllInterceptors() { 145 synchronized (myRegistryMutex) { 146 unregisterInterceptors(myAnonymousInvokers.values()); 147 unregisterInterceptors(myGlobalInvokers.values()); 148 unregisterInterceptors(myInterceptors); 149 } 150 } 151 152 @Override 153 public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) { 154 if (theInterceptors != null) { 155 new ArrayList<>(theInterceptors).forEach(t -> unregisterInterceptor(t)); 156 } 157 } 158 159 @Override 160 public void registerInterceptors(@Nullable Collection<?> theInterceptors) { 161 if (theInterceptors != null) { 162 theInterceptors.forEach(t -> registerInterceptor(t)); 163 } 164 } 165 166 @Override 167 public void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction) { 168 unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers); 169 unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers); 170 } 171 172 private void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction, ListMultimap<POINTCUT, BaseInvoker> theGlobalInvokers) { 173 theGlobalInvokers.entries().removeIf(t -> theShouldUnregisterFunction.test(t.getValue().getInterceptor())); 174 } 175 176 @Override 177 public boolean registerThreadLocalInterceptor(Object theInterceptor) { 178 if (!myThreadlocalInvokersEnabled) { 179 return false; 180 } 181 ListMultimap<POINTCUT, BaseInvoker> invokers = getThreadLocalInvokerMultimap(); 182 scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers); 183 return !invokers.isEmpty(); 184 185 } 186 187 @Override 188 public void unregisterThreadLocalInterceptor(Object theInterceptor) { 189 if (myThreadlocalInvokersEnabled) { 190 ListMultimap<POINTCUT, BaseInvoker> invokers = getThreadLocalInvokerMultimap(); 191 invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); 192 if (invokers.isEmpty()) { 193 myThreadlocalInvokers.remove(); 194 } 195 } 196 } 197 198 private ListMultimap<POINTCUT, BaseInvoker> getThreadLocalInvokerMultimap() { 199 ListMultimap<POINTCUT, BaseInvoker> invokers = myThreadlocalInvokers.get(); 200 if (invokers == null) { 201 invokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); 202 myThreadlocalInvokers.set(invokers); 203 } 204 return invokers; 205 } 206 207 @Override 208 public boolean registerInterceptor(Object theInterceptor) { 209 synchronized (myRegistryMutex) { 210 211 if (isInterceptorAlreadyRegistered(theInterceptor)) { 212 return false; 213 } 214 215 List<HookInvoker> addedInvokers = scanInterceptorAndAddToInvokerMultimap(theInterceptor, myGlobalInvokers); 216 if (addedInvokers.isEmpty()) { 217 if (myWarnOnInterceptorWithNoHooks) { 218 ourLog.warn("Interceptor registered with no valid hooks - Type was: {}", theInterceptor.getClass().getName()); 219 } 220 return false; 221 } 222 223 // Add to the global list 224 myInterceptors.add(theInterceptor); 225 sortByOrderAnnotation(myInterceptors); 226 227 return true; 228 } 229 } 230 231 private boolean isInterceptorAlreadyRegistered(Object theInterceptor) { 232 for (Object next : myInterceptors) { 233 if (next == theInterceptor) { 234 return true; 235 } 236 } 237 return false; 238 } 239 240 @Override 241 public boolean unregisterInterceptor(Object theInterceptor) { 242 synchronized (myRegistryMutex) { 243 boolean removed = myInterceptors.removeIf(t -> t == theInterceptor); 244 removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); 245 removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); 246 return removed; 247 } 248 } 249 250 private void sortByOrderAnnotation(List<Object> theObjects) { 251 IdentityHashMap<Object, Integer> interceptorToOrder = new IdentityHashMap<>(); 252 for (Object next : theObjects) { 253 Interceptor orderAnnotation = next.getClass().getAnnotation(Interceptor.class); 254 int order = orderAnnotation != null ? orderAnnotation.order() : 0; 255 interceptorToOrder.put(next, order); 256 } 257 258 theObjects.sort((a, b) -> { 259 Integer orderA = interceptorToOrder.get(a); 260 Integer orderB = interceptorToOrder.get(b); 261 return orderA - orderB; 262 }); 263 } 264 265 @Override 266 public Object callHooksAndReturnObject(POINTCUT thePointcut, HookParams theParams) { 267 assert haveAppropriateParams(thePointcut, theParams); 268 assert thePointcut.getReturnType() != void.class; 269 270 return doCallHooks(thePointcut, theParams, null); 271 } 272 273 @Override 274 public boolean hasHooks(POINTCUT thePointcut) { 275 return myGlobalInvokers.containsKey(thePointcut) 276 || myAnonymousInvokers.containsKey(thePointcut) 277 || hasThreadLocalHooks(thePointcut); 278 } 279 280 private boolean hasThreadLocalHooks(POINTCUT thePointcut) { 281 ListMultimap<POINTCUT, BaseInvoker> hooks = myThreadlocalInvokersEnabled ? myThreadlocalInvokers.get() : null; 282 return hooks != null && hooks.containsKey(thePointcut); 283 } 284 285 @Override 286 public boolean callHooks(POINTCUT thePointcut, HookParams theParams) { 287 assert haveAppropriateParams(thePointcut, theParams); 288 assert thePointcut.getReturnType() == void.class || thePointcut.getReturnType() == boolean.class; 289 290 Object retValObj = doCallHooks(thePointcut, theParams, true); 291 return (Boolean) retValObj; 292 } 293 294 private Object doCallHooks(POINTCUT thePointcut, HookParams theParams, Object theRetVal) { 295 // use new list for loop to avoid ConcurrentModificationException in case invoker gets added while looping 296 List<BaseInvoker> invokers = new ArrayList<>(getInvokersForPointcut(thePointcut)); 297 298 /* 299 * Call each hook in order 300 */ 301 for (BaseInvoker nextInvoker : invokers) { 302 Object nextOutcome = nextInvoker.invoke(theParams); 303 Class<?> pointcutReturnType = thePointcut.getReturnType(); 304 if (pointcutReturnType.equals(boolean.class)) { 305 Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome; 306 if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) { 307 ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker); 308 theRetVal = false; 309 break; 310 } 311 } else if (pointcutReturnType.equals(void.class) == false) { 312 if (nextOutcome != null) { 313 theRetVal = nextOutcome; 314 break; 315 } 316 } 317 } 318 319 return theRetVal; 320 } 321 322 @VisibleForTesting 323 List<Object> getInterceptorsWithInvokersForPointcut(POINTCUT thePointcut) { 324 return getInvokersForPointcut(thePointcut) 325 .stream() 326 .map(BaseInvoker::getInterceptor) 327 .collect(Collectors.toList()); 328 } 329 330 /** 331 * Returns an ordered list of invokers for the given pointcut. Note that 332 * a new and stable list is returned to.. do whatever you want with it. 333 */ 334 private List<BaseInvoker> getInvokersForPointcut(POINTCUT thePointcut) { 335 List<BaseInvoker> invokers; 336 337 synchronized (myRegistryMutex) { 338 List<BaseInvoker> globalInvokers = myGlobalInvokers.get(thePointcut); 339 List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut); 340 List<BaseInvoker> threadLocalInvokers = null; 341 if (myThreadlocalInvokersEnabled) { 342 ListMultimap<POINTCUT, BaseInvoker> pointcutToInvokers = myThreadlocalInvokers.get(); 343 if (pointcutToInvokers != null) { 344 threadLocalInvokers = pointcutToInvokers.get(thePointcut); 345 } 346 } 347 invokers = union(globalInvokers, anonymousInvokers, threadLocalInvokers); 348 } 349 350 return invokers; 351 } 352 353 /** 354 * First argument must be the global invoker list!! 355 */ 356 @SafeVarargs 357 private final List<BaseInvoker> union(List<BaseInvoker>... theInvokersLists) { 358 List<BaseInvoker> haveOne = null; 359 boolean haveMultiple = false; 360 for (List<BaseInvoker> nextInvokerList : theInvokersLists) { 361 if (nextInvokerList == null || nextInvokerList.isEmpty()) { 362 continue; 363 } 364 365 if (haveOne == null) { 366 haveOne = nextInvokerList; 367 } else { 368 haveMultiple = true; 369 } 370 } 371 372 if (haveOne == null) { 373 return Collections.emptyList(); 374 } 375 376 List<BaseInvoker> retVal; 377 378 if (haveMultiple == false) { 379 380 // The global list doesn't need to be sorted every time since it's sorted on 381 // insertion each time. Doing so is a waste of cycles.. 382 if (haveOne == theInvokersLists[0]) { 383 retVal = haveOne; 384 } else { 385 retVal = new ArrayList<>(haveOne); 386 retVal.sort(Comparator.naturalOrder()); 387 } 388 389 } else { 390 391 retVal = Arrays 392 .stream(theInvokersLists) 393 .filter(t -> t != null) 394 .flatMap(t -> t.stream()) 395 .sorted() 396 .collect(Collectors.toList()); 397 398 } 399 400 return retVal; 401 } 402 403 /** 404 * Only call this when assertions are enabled, it's expensive 405 */ 406 boolean haveAppropriateParams(POINTCUT thePointcut, HookParams theParams) { 407 if (theParams.getParamsForType().values().size() != thePointcut.getParameterTypes().size()) { 408 throw new IllegalArgumentException(Msg.code(1909) + String.format("Wrong number of params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), theParams.getParamsForType().values().stream().map(t -> t != null ? t.getClass().getSimpleName() : "null").sorted().collect(Collectors.toList()))); 409 } 410 411 List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes()); 412 413 ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType(); 414 for (Class<?> nextTypeClass : givenTypes.keySet()) { 415 String nextTypeName = nextTypeClass.getName(); 416 for (Object nextParamValue : givenTypes.get(nextTypeClass)) { 417 Validate.isTrue(nextParamValue == null || nextTypeClass.isAssignableFrom(nextParamValue.getClass()), "Invalid params for pointcut %s - %s is not of type %s", thePointcut.name(), nextParamValue != null ? nextParamValue.getClass() : "null", nextTypeClass); 418 Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName); 419 } 420 } 421 422 return true; 423 } 424 425 private List<HookInvoker> scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap<POINTCUT, BaseInvoker> theInvokers) { 426 Class<?> interceptorClass = theInterceptor.getClass(); 427 int typeOrder = determineOrder(interceptorClass); 428 429 List<HookInvoker> addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder); 430 431 // Invoke the REGISTERED pointcut for any added hooks 432 addedInvokers.stream() 433 .filter(t -> Pointcut.INTERCEPTOR_REGISTERED.equals(t.getPointcut())) 434 .forEach(t -> t.invoke(new HookParams())); 435 436 // Register the interceptor and its various hooks 437 for (HookInvoker nextAddedHook : addedInvokers) { 438 IPointcut nextPointcut = nextAddedHook.getPointcut(); 439 if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) { 440 continue; 441 } 442 theInvokers.put((POINTCUT) nextPointcut, nextAddedHook); 443 } 444 445 // Make sure we're always sorted according to the order declared in 446 // @Order 447 for (IPointcut nextPointcut : theInvokers.keys()) { 448 List<BaseInvoker> nextInvokerList = theInvokers.get((POINTCUT) nextPointcut); 449 nextInvokerList.sort(Comparator.naturalOrder()); 450 } 451 452 return addedInvokers; 453 } 454 455 /** 456 * @return Returns a list of any added invokers 457 */ 458 private List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) { 459 ArrayList<HookInvoker> retVal = new ArrayList<>(); 460 for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) { 461 Optional<HookDescriptor> hook = scanForHook(nextMethod); 462 463 if (hook.isPresent()) { 464 int methodOrder = theTypeOrder; 465 int methodOrderAnnotation = hook.get().getOrder(); 466 if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) { 467 methodOrder = methodOrderAnnotation; 468 } 469 470 retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder)); 471 } 472 } 473 474 return retVal; 475 } 476 477 protected abstract Optional<HookDescriptor> scanForHook(Method nextMethod); 478 479 protected abstract static class BaseInvoker implements Comparable<BaseInvoker> { 480 481 private final int myOrder; 482 private final Object myInterceptor; 483 484 BaseInvoker(Object theInterceptor, int theOrder) { 485 myInterceptor = theInterceptor; 486 myOrder = theOrder; 487 } 488 489 public Object getInterceptor() { 490 return myInterceptor; 491 } 492 493 abstract Object invoke(HookParams theParams); 494 495 @Override 496 public int compareTo(BaseInvoker theInvoker) { 497 return myOrder - theInvoker.myOrder; 498 } 499 } 500 501 private static class HookInvoker extends BaseInvoker { 502 503 private final Method myMethod; 504 private final Class<?>[] myParameterTypes; 505 private final int[] myParameterIndexes; 506 private final IPointcut myPointcut; 507 508 /** 509 * Constructor 510 */ 511 private HookInvoker(HookDescriptor theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) { 512 super(theInterceptor, theOrder); 513 myPointcut = theHook.getPointcut(); 514 myParameterTypes = theHookMethod.getParameterTypes(); 515 myMethod = theHookMethod; 516 517 Class<?> returnType = theHookMethod.getReturnType(); 518 if (myPointcut.getReturnType().equals(boolean.class)) { 519 Validate.isTrue(boolean.class.equals(returnType) || void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod); 520 } else if (myPointcut.getReturnType().equals(void.class)) { 521 Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod); 522 } else { 523 Validate.isTrue(myPointcut.getReturnType().isAssignableFrom(returnType) || void.class.equals(returnType), "Method does not return %s or void: %s", myPointcut.getReturnType(), theHookMethod); 524 } 525 526 myParameterIndexes = new int[myParameterTypes.length]; 527 Map<Class<?>, AtomicInteger> typeToCount = new HashMap<>(); 528 for (int i = 0; i < myParameterTypes.length; i++) { 529 AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0)); 530 myParameterIndexes[i] = counter.getAndIncrement(); 531 } 532 533 myMethod.setAccessible(true); 534 } 535 536 @Override 537 public String toString() { 538 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 539 .append("method", myMethod) 540 .toString(); 541 } 542 543 public IPointcut getPointcut() { 544 return myPointcut; 545 } 546 547 /** 548 * @return Returns true/false if the hook method returns a boolean, returns true otherwise 549 */ 550 @Override 551 Object invoke(HookParams theParams) { 552 553 Object[] args = new Object[myParameterTypes.length]; 554 for (int i = 0; i < myParameterTypes.length; i++) { 555 Class<?> nextParamType = myParameterTypes[i]; 556 if (nextParamType.equals(Pointcut.class)) { 557 args[i] = myPointcut; 558 } else { 559 int nextParamIndex = myParameterIndexes[i]; 560 Object nextParamValue = theParams.get(nextParamType, nextParamIndex); 561 args[i] = nextParamValue; 562 } 563 } 564 565 // Invoke the method 566 try { 567 return myMethod.invoke(getInterceptor(), args); 568 } catch (InvocationTargetException e) { 569 Throwable targetException = e.getTargetException(); 570 if (myPointcut.isShouldLogAndSwallowException(targetException)) { 571 ourLog.error("Exception thrown by interceptor: " + targetException.toString(), targetException); 572 return null; 573 } 574 575 if (targetException instanceof RuntimeException) { 576 throw ((RuntimeException) targetException); 577 } else { 578 throw new InternalErrorException(Msg.code(1910) + "Failure invoking interceptor for pointcut(s) " + getPointcut(), targetException); 579 } 580 } catch (Exception e) { 581 throw new InternalErrorException(Msg.code(1911) + e); 582 } 583 584 } 585 586 } 587 588 protected static class HookDescriptor { 589 590 private final IPointcut myPointcut; 591 private final int myOrder; 592 593 public HookDescriptor(IPointcut thePointcut, int theOrder) { 594 myPointcut = thePointcut; 595 myOrder = theOrder; 596 } 597 598 IPointcut getPointcut() { 599 return myPointcut; 600 } 601 602 int getOrder() { 603 return myOrder; 604 } 605 606 } 607 608 protected static <T extends Annotation> Optional<T> findAnnotation(AnnotatedElement theObject, Class<T> theHookClass) { 609 T annotation; 610 if (theObject instanceof Method) { 611 annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true); 612 } else { 613 annotation = theObject.getAnnotation(theHookClass); 614 } 615 return Optional.ofNullable(annotation); 616 } 617 618 private static int determineOrder(Class<?> theInterceptorClass) { 619 int typeOrder = Interceptor.DEFAULT_ORDER; 620 Optional<Interceptor> typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class); 621 if (typeOrderAnnotation.isPresent()) { 622 typeOrder = typeOrderAnnotation.get().order(); 623 } 624 return typeOrder; 625 } 626 627 private static String toErrorString(List<String> theParameterTypes) { 628 return theParameterTypes 629 .stream() 630 .sorted() 631 .collect(Collectors.joining(",")); 632 } 633 634}