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 LOGGER.debug("Processing class {}", className); 225 226 // Class isn't in a controlled package, so it doesn't get transformed 227 // and should go for the root context, which is never thrown out. 228 if (!root.getPlasticManager().shouldInterceptClassLoading(className)) 229 { 230 context = root; 231 } else { 232 if (!productionMode && ( 233 !componentDependencyRegistry.contains(className) || 234 !multipleClassLoaders)) 235 { 236 context = unknownContextProvider.get(); 237 } 238 else 239 { 240 241 alreadyProcessed.add(className); 242 243 // Sorting dependencies alphabetically so we have consistent results. 244 List<String> dependencies = new ArrayList<>(getDependenciesWithoutPages(className)); 245 Collections.sort(dependencies); 246 247 // Process dependencies depth-first 248 for (String dependency : dependencies) 249 { 250 // Avoid infinite recursion loops 251 if (!alreadyProcessed.contains(dependency)) 252 { 253 processUsingDependencies(dependency, root, unknownContextProvider, 254 plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed, false); 255 } 256 } 257 258 // Collect context dependencies 259 Set<PageClassLoaderContext> contextDependencies = new HashSet<>(); 260 for (String dependency : dependencies) 261 { 262 PageClassLoaderContext dependencyContext = root.findByClassName(dependency); 263 // Avoid infinite recursion loops 264 if (!alreadyProcessed.contains(dependency)) 265 { 266 if (dependencyContext == null) 267 { 268 dependencyContext = processUsingDependencies(dependency, root, unknownContextProvider, 269 plasticProxyFactoryProvider, classesToInvalidate, alreadyProcessed); 270 271 } 272 if (!dependencyContext.isRoot()) 273 { 274 contextDependencies.add(dependencyContext); 275 } 276 } 277 } 278 279 if (!multipleClassLoaders) 280 { 281 context = root; 282 } 283 else if (contextDependencies.size() == 0) 284 { 285 context = new PageClassLoaderContext( 286 getContextName(className), 287 root, 288 Collections.singleton(className), 289 plasticProxyFactoryProvider.apply(root.getClassLoader()), 290 this::get); 291 } 292 else 293 { 294 PageClassLoaderContext parentContext; 295 if (contextDependencies.size() == 1) 296 { 297 parentContext = contextDependencies.iterator().next(); 298 } 299 else 300 { 301 parentContext = merge(contextDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); 302 } 303 context = new PageClassLoaderContext( 304 getContextName(className), 305 parentContext, 306 Collections.singleton(className), 307 plasticProxyFactoryProvider.apply(parentContext.getClassLoader()), 308 this::get); 309 } 310 311 if (multipleClassLoaders) 312 { 313 context.getParent().addChild(context); 314 } 315 316 // Ensure non-page class is initialized in the correct context and classloader. 317 // Pages get their own context and classloader, so this initialization 318 // is both non-needed and a cause for an NPE if it happens. 319 if (!componentClassResolver.isPage(className) 320 || componentDependencyRegistry.getDependencies(className, DependencyType.USAGE).isEmpty()) 321 { 322 loadClass(className, context); 323 } 324 325 if (multipleClassLoaders) 326 { 327 LOGGER.debug("New context: {}", context); 328 } 329 330 } 331 } 332 333 } 334 context.addClass(className); 335 336 return context; 337 } 338 339 private Set<String> getDependenciesWithoutPages(String className) 340 { 341 Set<String> dependencies = new HashSet<>(); 342 dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.USAGE)); 343 dependencies.addAll(componentDependencyRegistry.getDependencies(className, DependencyType.SUPERCLASS)); 344 return Collections.unmodifiableSet(dependencies); 345 } 346 347 private Class<?> loadClass(String className, PageClassLoaderContext context) 348 { 349 try 350 { 351 final ClassLoader classLoader = context.getPlasticManager().getClassLoader(); 352 return classLoader.loadClass(className); 353 } catch (Exception e) { 354 throw new RuntimeException(e); 355 } 356 } 357 358 private PageClassLoaderContext merge( 359 Set<PageClassLoaderContext> contextDependencies, 360 Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, 361 PageClassLoaderContext root, Set<String> classesToInvalidate) 362 { 363 364 NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1); 365 366 if (LOGGER.isDebugEnabled()) 367 { 368 369 LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get()); 370 371 String classes; 372 StringBuilder builder = new StringBuilder(); 373 builder.append("Merging the following page classloader contexts into one:\n"); 374 for (PageClassLoaderContext context : contextDependencies) 375 { 376 classes = context.getClassNames().stream() 377 .map(this::getContextName) 378 .sorted() 379 .collect(Collectors.joining(", ")); 380 builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes)); 381 } 382 LOGGER.debug(builder.toString().trim()); 383 } 384 385 Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>(); 386 for (PageClassLoaderContext context : contextDependencies) 387 { 388 allContextsIncludingDescendents.add(context); 389 allContextsIncludingDescendents.addAll(context.getDescendents()); 390 } 391 392 PageClassLoaderContext merged; 393 394 // Collect the classes in these dependencies, then invalidate the contexts 395 396 Set<PageClassLoaderContext> furtherDependencies = new HashSet<>(); 397 398 Set<String> classNames = new HashSet<>(); 399 400 for (PageClassLoaderContext context : contextDependencies) 401 { 402 if (!context.isRoot()) 403 { 404 classNames.addAll(context.getClassNames()); 405 } 406 final PageClassLoaderContext parent = context.getParent(); 407 // We don't want the merged context to have a further dependency on 408 // the root context (it's not mergeable) nor on itself. 409 if (!parent.isRoot() && 410 !allContextsIncludingDescendents.contains(parent)) 411 { 412 furtherDependencies.add(parent); 413 } 414 } 415 416 final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream() 417 .filter(c -> !c.isRoot()) 418 .collect(Collectors.toList()); 419 420 if (!contextsToInvalidate.isEmpty()) 421 { 422 classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()]))); 423 } 424 425 PageClassLoaderContext parent; 426 427 // No context dependencies, so parent is going to be the root one 428 if (furtherDependencies.size() == 0) 429 { 430 parent = root; 431 } 432 else 433 { 434 // Single shared context dependency, so it's our parent 435 if (furtherDependencies.size() == 1) 436 { 437 parent = furtherDependencies.iterator().next(); 438 } 439 // No single context dependency, so we'll need to recursively merge it 440 // so we can have a single parent. 441 else 442 { 443 parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); 444 LOGGER.debug("New context: {}", parent); 445 } 446 } 447 448 merged = new PageClassLoaderContext( 449 "merged " + MERGED_COUNTER.getAndIncrement(), 450 parent, 451 classNames, 452 plasticProxyFactoryProvider.apply(parent.getClassLoader()), 453 this::get); 454 455 parent.addChild(merged); 456 457// for (String className : classNames) 458// { 459// loadClass(className, merged); 460// } 461 462 NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1); 463 if (LOGGER.isDebugEnabled()) 464 { 465 LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get()); 466 } 467 468 return merged; 469 } 470 471 @Override 472 public void clear(String className) 473 { 474 final PageClassLoaderContext context = root.findByClassName(className); 475 if (context != null) 476 { 477// invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context))); 478 invalidate(context); 479 } 480 } 481 482 private String getContextName(String className) 483 { 484 String contextName = componentClassResolver.getLogicalName(className); 485 if (contextName == null) 486 { 487 contextName = className; 488 } 489 return contextName; 490 } 491 492 @Override 493 public Set<String> invalidate(PageClassLoaderContext ... contexts) 494 { 495 Set<String> classNames = new HashSet<>(); 496 for (PageClassLoaderContext context : contexts) { 497 addClassNames(context, classNames); 498 context.invalidate(); 499 if (context.getParent() != null) 500 { 501 context.getParent().removeChild(context); 502 } 503 } 504 return classNames; 505 } 506 507 private List<String> listen(List<String> resources) 508 { 509 510 List<String> returnValue; 511 512 if (!multipleClassLoaders) 513 { 514 for (PageClassLoaderContext context : root.getChildren()) 515 { 516 context.invalidate(); 517 } 518 returnValue = Collections.emptyList(); 519 } 520 else if (INVALIDATING_CONTEXT.get()) 521 { 522 returnValue = Collections.emptyList(); 523 } 524 else 525 { 526 527 Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>(); 528 for (String resource : resources) 529 { 530 PageClassLoaderContext context = root.findByClassName(resource); 531 if (context != null && !context.isRoot()) 532 { 533 contextsToInvalidate.add(context); 534 } 535 } 536 537 Set<String> furtherResources = invalidate(contextsToInvalidate.toArray( 538 new PageClassLoaderContext[contextsToInvalidate.size()])); 539 540 // We don't want to invalidate resources more than once 541 furtherResources.removeAll(resources); 542 543 returnValue = new ArrayList<>(furtherResources); 544 } 545 546 return returnValue; 547 548 } 549 550 @SuppressWarnings("unchecked") 551 @Override 552 public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) { 553 markAsInvalidatingContext(); 554 if (multipleClassLoaders) 555 { 556 final Set<String> classNames = invalidate(contexts); 557 invalidate(classNames); 558 } 559 else 560 { 561 invalidate(Collections.EMPTY_SET); 562 } 563 markAsNotInvalidatingContext(); 564 } 565 566 private void markAsNotInvalidatingContext() { 567 INVALIDATING_CONTEXT.set(false); 568 } 569 570 private void markAsInvalidatingContext() { 571 INVALIDATING_CONTEXT.set(true); 572 } 573 574 private void invalidate(Set<String> classesToInvalidate) { 575 if (!classesToInvalidate.isEmpty()) 576 { 577 LOGGER.debug("Invalidating classes {}", classesToInvalidate); 578 markAsInvalidatingContext(); 579 final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate); 580 581 componentDependencyRegistry.disableInvalidations(); 582 583 try 584 { 585 // TODO: do we really need both invalidation hubs to be invoked here? 586 invalidationHub.fireInvalidationEvent(classesToInvalidateAsList); 587 componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList); 588 markAsNotInvalidatingContext(); 589 } 590 finally 591 { 592 componentDependencyRegistry.enableInvalidations(); 593 } 594 595 } 596 } 597 598 private void addClassNames(PageClassLoaderContext context, Set<String> classNames) { 599 classNames.addAll(context.getClassNames()); 600 for (PageClassLoaderContext child : context.getChildren()) { 601 addClassNames(child, classNames); 602 } 603 } 604 605 @Override 606 public PageClassLoaderContext getRoot() { 607 return root; 608 } 609 610 @Override 611 public boolean isMerging() 612 { 613 return NESTED_MERGE_COUNT.get() > 0; 614 } 615 616 @Override 617 public void clear() 618 { 619 } 620 621 @Override 622 public Class<?> getClassInstance(Class<?> clasz, String pageName) 623 { 624 final String className = clasz.getName(); 625 PageClassLoaderContext context = root.findByClassName(className); 626 if (context == null) 627 { 628 context = get(className); 629 } 630 try 631 { 632 clasz = context.getProxyFactory().getClassLoader().loadClass(className); 633 } catch (ClassNotFoundException e) 634 { 635 throw new TapestryException(e.getMessage(), e); 636 } 637 return clasz; 638 } 639 640 @Override 641 public void preload() 642 { 643 644 final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, 645 Collections.emptySet(), 646 plasticProxyFactoryProvider.apply(root.getClassLoader()), 647 this::get); 648 649 final List<String> pageNames = componentClassResolver.getPageNames(); 650 final List<String> classNames = new ArrayList<>(pageNames.size()); 651 652 long start = System.currentTimeMillis(); 653 654 LOGGER.info("Preloading dependency information for {} pages", pageNames.size()); 655 656 for (String page : pageNames) 657 { 658 try 659 { 660 final String className = componentClassResolver.resolvePageNameToClassName(page); 661 componentDependencyRegistry.register(context.getClassLoader().loadClass(className)); 662 classNames.add(className); 663 } catch (ClassNotFoundException e) 664 { 665 throw new RuntimeException(e); 666 } 667 catch (Exception e) 668 { 669 LOGGER.warn("Exception while preloading page " + page, e); 670 } 671 } 672 673 long finish = System.currentTimeMillis(); 674 675 if (LOGGER.isInfoEnabled()) 676 { 677 LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0)); 678 } 679 680 context.invalidate(); 681 682 LOGGER.info("Starting preloading page classloader contexts"); 683 684 start = System.currentTimeMillis(); 685 686 for (int i = 0; i < 10; i++) 687 { 688 for (String className : classNames) 689 { 690 get(className); 691 } 692 } 693 694 finish = System.currentTimeMillis(); 695 696 if (LOGGER.isInfoEnabled()) 697 { 698 LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0)); 699 } 700 701 } 702 703}