001// Copyright 2023 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014package org.apache.tapestry5.services.pageload; 015 016import java.util.ArrayList; 017import java.util.Collections; 018import java.util.HashSet; 019import java.util.List; 020import java.util.Objects; 021import java.util.Set; 022import java.util.concurrent.atomic.AtomicInteger; 023import java.util.function.Function; 024import java.util.function.Supplier; 025import java.util.stream.Collectors; 026 027import org.apache.tapestry5.SymbolConstants; 028import org.apache.tapestry5.commons.internal.util.TapestryException; 029import org.apache.tapestry5.commons.services.InvalidationEventHub; 030import org.apache.tapestry5.commons.services.PlasticProxyFactory; 031import org.apache.tapestry5.internal.services.ComponentDependencyRegistry; 032import org.apache.tapestry5.internal.services.ComponentDependencyRegistry.DependencyType; 033import org.apache.tapestry5.internal.services.InternalComponentInvalidationEventHub; 034import org.apache.tapestry5.ioc.annotations.ComponentClasses; 035import org.apache.tapestry5.ioc.annotations.Symbol; 036import org.apache.tapestry5.plastic.PlasticUtils; 037import org.apache.tapestry5.services.ComponentClassResolver; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041/** 042 * Default {@linkplain PageClassLoaderContextManager} implementation. 043 * 044 * @since 5.8.3 045 */ 046public class PageClassLoaderContextManagerImpl implements PageClassLoaderContextManager 047{ 048 049 private static final Logger LOGGER = LoggerFactory.getLogger(PageClassLoaderContextManager.class); 050 051 private final ComponentDependencyRegistry componentDependencyRegistry; 052 053 private final ComponentClassResolver componentClassResolver; 054 055 private final InternalComponentInvalidationEventHub invalidationHub; 056 057 private final InvalidationEventHub componentClassesInvalidationEventHub; 058 059 private final boolean multipleClassLoaders; 060 061 private final boolean productionMode; 062 063 private final static ThreadLocal<Integer> NESTED_MERGE_COUNT = ThreadLocal.withInitial(() -> 0); 064 065 private final static ThreadLocal<Boolean> INVALIDATING_CONTEXT = ThreadLocal.withInitial(() -> false); 066 067 private static final AtomicInteger MERGED_COUNTER = new AtomicInteger(1); 068 069 private Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider; 070 071 private PageClassLoaderContext root; 072 073 public PageClassLoaderContextManagerImpl( 074 final ComponentDependencyRegistry componentDependencyRegistry, 075 final ComponentClassResolver componentClassResolver, 076 final InternalComponentInvalidationEventHub invalidationHub, 077 final @ComponentClasses InvalidationEventHub componentClassesInvalidationEventHub, 078 final @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, 079 final @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassLoaders) 080 { 081 super(); 082 this.componentDependencyRegistry = componentDependencyRegistry; 083 this.componentClassResolver = componentClassResolver; 084 this.invalidationHub = invalidationHub; 085 this.componentClassesInvalidationEventHub = componentClassesInvalidationEventHub; 086 this.multipleClassLoaders = multipleClassLoaders; 087 this.productionMode = productionMode; 088 invalidationHub.addInvalidationCallback(this::listen); 089 NESTED_MERGE_COUNT.set(0); 090 } 091 092 @Override 093 public void invalidateUnknownContext() 094 { 095 synchronized (this) { 096 markAsNotInvalidatingContext(); 097 for (PageClassLoaderContext context : root.getChildren()) 098 { 099 if (context.isUnknown()) 100 { 101 invalidateAndFireInvalidationEvents(context); 102 break; 103 } 104 } 105 } 106 } 107 108 @Override 109 public void initialize( 110 final PageClassLoaderContext root, 111 final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider) 112 { 113 if (this.root != null) 114 { 115 throw new IllegalStateException("PageClassloaderContextManager.initialize() can only be called once"); 116 } 117 Objects.requireNonNull(root); 118 Objects.requireNonNull(plasticProxyFactoryProvider); 119 this.root = root; 120 this.plasticProxyFactoryProvider = plasticProxyFactoryProvider; 121 LOGGER.info("Root context: {}", root); 122 } 123 124 @Override 125 public synchronized PageClassLoaderContext get(final String className) 126 { 127 PageClassLoaderContext context; 128 129 final String enclosingClassName = PlasticUtils.getEnclosingClassName(className); 130 context = root.findByClassName(enclosingClassName); 131 132 if (context == null) 133 { 134 Set<String> classesToInvalidate = new HashSet<>(); 135 136 context = processUsingDependencies( 137 enclosingClassName, 138 root, 139 () -> getUnknownContext(root, plasticProxyFactoryProvider), 140 plasticProxyFactoryProvider, 141 classesToInvalidate); 142 143 if (!classesToInvalidate.isEmpty()) 144 { 145 invalidate(classesToInvalidate); 146 } 147 148 if (!className.equals(enclosingClassName)) 149 { 150 loadClass(className, context); 151 } 152 153 } 154 155 return context; 156 157 } 158 159 private PageClassLoaderContext getUnknownContext(final PageClassLoaderContext root, 160 final Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider) 161 { 162 163 PageClassLoaderContext unknownContext = null; 164 165 for (PageClassLoaderContext child : root.getChildren()) 166 { 167 if (child.getName().equals(PageClassLoaderContext.UNKOWN_CONTEXT_NAME)) 168 { 169 unknownContext = child; 170 break; 171 } 172 } 173 174 if (unknownContext == null) 175 { 176 unknownContext = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, 177 Collections.emptySet(), 178 plasticProxyFactoryProvider.apply(root.getClassLoader()), 179 this::get); 180 root.addChild(unknownContext); 181 if (multipleClassLoaders) 182 { 183 LOGGER.debug("Unknown context: {}", unknownContext); 184 } 185 } 186 return unknownContext; 187 } 188 189 private PageClassLoaderContext processUsingDependencies( 190 String className, 191 PageClassLoaderContext root, 192 Supplier<PageClassLoaderContext> unknownContextProvider, 193 Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, Set<String> classesToInvalidate) 194 { 195 return processUsingDependencies(className, root, unknownContextProvider, plasticProxyFactoryProvider, classesToInvalidate, new HashSet<>()); 196 } 197 198 private PageClassLoaderContext processUsingDependencies( 199 String className, 200 PageClassLoaderContext root, 201 Supplier<PageClassLoaderContext> unknownContextProvider, 202 Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, 203 Set<String> classesToInvalidate, 204 Set<String> alreadyProcessed) 205 { 206 return processUsingDependencies(className, root, unknownContextProvider, 207 plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, true); 208 } 209 210 211 private PageClassLoaderContext processUsingDependencies( 212 String className, 213 PageClassLoaderContext root, 214 Supplier<PageClassLoaderContext> unknownContextProvider, 215 Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, 216 Set<String> classesToInvalidate, 217 Set<String> alreadyProcessed, 218 boolean processCircularDependencies) 219 { 220 PageClassLoaderContext context = root.findByClassName(className); 221 if (context == null) 222 { 223 224 // Class isn't in a controlled package, so it doesn't get transformed 225 // and should go for the root context, which is never thrown out. 226 if (!root.getPlasticManager().shouldInterceptClassLoading(className)) 227 { 228 context = root; 229 } else { 230 if (!productionMode && ( 231 !componentDependencyRegistry.contains(className) || 232 !multipleClassLoaders)) 233 { 234 context = unknownContextProvider.get(); 235 } 236 else 237 { 238 239 alreadyProcessed.add(className); 240 241 // Sorting dependencies alphabetically so we have consistent results. 242 List<String> dependencies = new ArrayList<>(getDependenciesWithoutPages(className)); 243 Collections.sort(dependencies); 244 245 // Process dependencies depth-first 246 for (String dependency : dependencies) 247 { 248 // Avoid infinite recursion loops 249 if (!alreadyProcessed.contains(dependency)/* && 250 !circularDependencies.contains(dependency)*/) 251 { 252 processUsingDependencies(dependency, root, unknownContextProvider, 253 plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, false); 254 } 255 } 256 257 // Collect context dependencies 258 Set<PageClassLoaderContext> contextDependencies = new HashSet<>(); 259 for (String dependency : dependencies) 260 { 261 PageClassLoaderContext dependencyContext = root.findByClassName(dependency); 262 if (dependencyContext == null) 263 { 264 dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider, 265 plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed); 266 267 } 268 if (!dependencyContext.isRoot()) 269 { 270 contextDependencies.add(dependencyContext); 271 } 272 } 273 274 if (!multipleClassLoaders) 275 { 276 context = root; 277 } 278 else if (contextDependencies.size() == 0) 279 { 280 context = new PageClassLoaderContext( 281 getContextName(className), 282 root, 283 Collections.singleton(className), 284 plasticProxyFactoryProvider.apply(root.getClassLoader()), 285 this::get); 286 } 287 else 288 { 289 PageClassLoaderContext parentContext; 290 if (contextDependencies.size() == 1) 291 { 292 parentContext = contextDependencies.iterator().next(); 293 } 294 else 295 { 296 parentContext = merge(contextDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); 297 } 298 context = new PageClassLoaderContext( 299 getContextName(className), 300 parentContext, 301 Collections.singleton(className), 302 plasticProxyFactoryProvider.apply(parentContext.getClassLoader()), 303 this::get); 304 } 305 306 if (multipleClassLoaders) 307 { 308 context.getParent().addChild(context); 309 } 310 311 // Ensure non-page class is initialized in the correct context and classloader. 312 // Pages get their own context and classloader, so this initialization 313 // is both non-needed and a cause for an NPE if it happens. 314 if (!componentClassResolver.isPage(className) 315 || componentDependencyRegistry.getDependencies(className, DependencyType.USAGE).isEmpty()) 316 { 317 loadClass(className, context); 318 } 319 320 if (multipleClassLoaders) 321 { 322 LOGGER.debug("New context: {}", context); 323 } 324 325 } 326 } 327 328 } 329 context.addClass(className); 330 331 return context; 332 } 333 334 private Set<String> getDependenciesWithoutPages(String className) 335 { 336 Set<String> dependencies = new HashSet<>(); 337 dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE)); 338 dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)); 339 return Collections.unmodifiableSet(dependencies); 340 } 341 342 private Class<?> loadClass(String className, PageClassLoaderContext context) 343 { 344 try 345 { 346 final ClassLoader classLoader = context.getPlasticManager().getClassLoader(); 347 return classLoader.loadClass(className); 348 } catch (Exception e) { 349 throw new RuntimeException(e); 350 } 351 } 352 353 private PageClassLoaderContext merge( 354 Set<PageClassLoaderContext> contextDependencies, 355 Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, 356 PageClassLoaderContext root, Set<String> classesToInvalidate) 357 { 358 359 NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1); 360 361 if (LOGGER.isDebugEnabled()) 362 { 363 364 LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get()); 365 366 String classes; 367 StringBuilder builder = new StringBuilder(); 368 builder.append("Merging the following page classloader contexts into one:\n"); 369 for (PageClassLoaderContext context : contextDependencies) 370 { 371 classes = context.getClassNames().stream() 372 .map(this::getContextName) 373 .sorted() 374 .collect(Collectors.joining(", ")); 375 builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes)); 376 } 377 LOGGER.debug(builder.toString().trim()); 378 } 379 380 Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>(); 381 for (PageClassLoaderContext context : contextDependencies) 382 { 383 allContextsIncludingDescendents.add(context); 384 allContextsIncludingDescendents.addAll(context.getDescendents()); 385 } 386 387 PageClassLoaderContext merged; 388 389 // Collect the classes in these dependencies, then invalidate the contexts 390 391 Set<PageClassLoaderContext> furtherDependencies = new HashSet<>(); 392 393 Set<String> classNames = new HashSet<>(); 394 395 for (PageClassLoaderContext context : contextDependencies) 396 { 397 if (!context.isRoot()) 398 { 399 classNames.addAll(context.getClassNames()); 400 } 401 final PageClassLoaderContext parent = context.getParent(); 402 // We don't want the merged context to have a further dependency on 403 // the root context (it's not mergeable) nor on itself. 404 if (!parent.isRoot() && 405 !allContextsIncludingDescendents.contains(parent)) 406 { 407 furtherDependencies.add(parent); 408 } 409 } 410 411 final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream() 412 .filter(c -> !c.isRoot()) 413 .collect(Collectors.toList()); 414 415 if (!contextsToInvalidate.isEmpty()) 416 { 417 classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()]))); 418 } 419 420 PageClassLoaderContext parent; 421 422 // No context dependencies, so parent is going to be the root one 423 if (furtherDependencies.size() == 0) 424 { 425 parent = root; 426 } 427 else 428 { 429 // Single shared context dependency, so it's our parent 430 if (furtherDependencies.size() == 1) 431 { 432 parent = furtherDependencies.iterator().next(); 433 } 434 // No single context dependency, so we'll need to recursively merge it 435 // so we can have a single parent. 436 else 437 { 438 parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); 439 LOGGER.debug("New context: {}", parent); 440 } 441 } 442 443 merged = new PageClassLoaderContext( 444 "merged " + MERGED_COUNTER.getAndIncrement(), 445 parent, 446 classNames, 447 plasticProxyFactoryProvider.apply(parent.getClassLoader()), 448 this::get); 449 450 parent.addChild(merged); 451 452// for (String className : classNames) 453// { 454// loadClass(className, merged); 455// } 456 457 NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1); 458 if (LOGGER.isDebugEnabled()) 459 { 460 LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get()); 461 } 462 463 return merged; 464 } 465 466 @Override 467 public void clear(String className) 468 { 469 final PageClassLoaderContext context = root.findByClassName(className); 470 if (context != null) 471 { 472// invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context))); 473 invalidate(context); 474 } 475 } 476 477 private String getContextName(String className) 478 { 479 String contextName = componentClassResolver.getLogicalName(className); 480 if (contextName == null) 481 { 482 contextName = className; 483 } 484 return contextName; 485 } 486 487 @Override 488 public Set<String> invalidate(PageClassLoaderContext ... contexts) 489 { 490 Set<String> classNames = new HashSet<>(); 491 for (PageClassLoaderContext context : contexts) { 492 addClassNames(context, classNames); 493 context.invalidate(); 494 if (context.getParent() != null) 495 { 496 context.getParent().removeChild(context); 497 } 498 } 499 return classNames; 500 } 501 502 private List<String> listen(List<String> resources) 503 { 504 505 List<String> returnValue; 506 507 if (!multipleClassLoaders) 508 { 509 for (PageClassLoaderContext context : root.getChildren()) 510 { 511 context.invalidate(); 512 } 513 returnValue = Collections.emptyList(); 514 } 515 else if (INVALIDATING_CONTEXT.get()) 516 { 517 returnValue = Collections.emptyList(); 518 } 519 else 520 { 521 522 Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>(); 523 for (String resource : resources) 524 { 525 PageClassLoaderContext context = root.findByClassName(resource); 526 if (context != null && !context.isRoot()) 527 { 528 contextsToInvalidate.add(context); 529 } 530 } 531 532 Set<String> furtherResources = invalidate(contextsToInvalidate.toArray( 533 new PageClassLoaderContext[contextsToInvalidate.size()])); 534 535 // We don't want to invalidate resources more than once 536 furtherResources.removeAll(resources); 537 538 returnValue = new ArrayList<>(furtherResources); 539 } 540 541 return returnValue; 542 543 } 544 545 @SuppressWarnings("unchecked") 546 @Override 547 public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) { 548 markAsInvalidatingContext(); 549 if (multipleClassLoaders) 550 { 551 final Set<String> classNames = invalidate(contexts); 552 invalidate(classNames); 553 } 554 else 555 { 556 invalidate(Collections.EMPTY_SET); 557 } 558 markAsNotInvalidatingContext(); 559 } 560 561 private void markAsNotInvalidatingContext() { 562 INVALIDATING_CONTEXT.set(false); 563 } 564 565 private void markAsInvalidatingContext() { 566 INVALIDATING_CONTEXT.set(true); 567 } 568 569 private void invalidate(Set<String> classesToInvalidate) { 570 if (!classesToInvalidate.isEmpty()) 571 { 572 LOGGER.debug("Invalidating classes {}", classesToInvalidate); 573 markAsInvalidatingContext(); 574 final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate); 575 576 componentDependencyRegistry.disableInvalidations(); 577 578 try 579 { 580 // TODO: do we really need both invalidation hubs to be invoked here? 581 invalidationHub.fireInvalidationEvent(classesToInvalidateAsList); 582 componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList); 583 markAsNotInvalidatingContext(); 584 } 585 finally 586 { 587 componentDependencyRegistry.enableInvalidations(); 588 } 589 590 } 591 } 592 593 private void addClassNames(PageClassLoaderContext context, Set<String> classNames) { 594 classNames.addAll(context.getClassNames()); 595 for (PageClassLoaderContext child : context.getChildren()) { 596 addClassNames(child, classNames); 597 } 598 } 599 600 @Override 601 public PageClassLoaderContext getRoot() { 602 return root; 603 } 604 605 @Override 606 public boolean isMerging() 607 { 608 return NESTED_MERGE_COUNT.get() > 0; 609 } 610 611 @Override 612 public void clear() 613 { 614 } 615 616 @Override 617 public Class<?> getClassInstance(Class<?> clasz, String pageName) 618 { 619 final String className = clasz.getName(); 620 PageClassLoaderContext context = root.findByClassName(className); 621 if (context == null) 622 { 623 context = get(className); 624 } 625 try 626 { 627 clasz = context.getProxyFactory().getClassLoader().loadClass(className); 628 } catch (ClassNotFoundException e) 629 { 630 throw new TapestryException(e.getMessage(), e); 631 } 632 return clasz; 633 } 634 635 @Override 636 public void preload() 637 { 638 639 final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, 640 Collections.emptySet(), 641 plasticProxyFactoryProvider.apply(root.getClassLoader()), 642 this::get); 643 644 final List<String> pageNames = componentClassResolver.getPageNames(); 645 final List<String> classNames = new ArrayList<>(pageNames.size()); 646 647 long start = System.currentTimeMillis(); 648 649 LOGGER.info("Preloading dependency information for {} pages", pageNames.size()); 650 651 for (String page : pageNames) 652 { 653 try 654 { 655 final String className = componentClassResolver.resolvePageNameToClassName(page); 656 componentDependencyRegistry.register(context.getClassLoader().loadClass(className)); 657 classNames.add(className); 658 } catch (ClassNotFoundException e) 659 { 660 throw new RuntimeException(e); 661 } 662 catch (Exception e) 663 { 664 LOGGER.warn("Exception while preloading page " + page, e); 665 } 666 } 667 668 long finish = System.currentTimeMillis(); 669 670 if (LOGGER.isInfoEnabled()) 671 { 672 LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0)); 673 } 674 675 context.invalidate(); 676 677 LOGGER.info("Starting preloading page classloader contexts"); 678 679 start = System.currentTimeMillis(); 680 681 for (int i = 0; i < 10; i++) 682 { 683 for (String className : classNames) 684 { 685 get(className); 686 } 687 } 688 689 finish = System.currentTimeMillis(); 690 691 if (LOGGER.isInfoEnabled()) 692 { 693 LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0)); 694 } 695 696 } 697 698}