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