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