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