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 return componentDependencyRegistry.getDependencies(className, DependencyType.USAGE); 329 } 330 331 private Class<?> loadClass(String className, PageClassLoaderContext context) 332 { 333 try 334 { 335 final ClassLoader classLoader = context.getPlasticManager().getClassLoader(); 336 return classLoader.loadClass(className); 337 } catch (Exception e) { 338 throw new RuntimeException(e); 339 } 340 } 341 342 private PageClassLoaderContext merge( 343 Set<PageClassLoaderContext> contextDependencies, 344 Function<ClassLoader, PlasticProxyFactory> plasticProxyFactoryProvider, 345 PageClassLoaderContext root, Set<String> classesToInvalidate) 346 { 347 348 NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() + 1); 349 350 if (LOGGER.isDebugEnabled()) 351 { 352 353 LOGGER.debug("Nested merge count going up to {}", NESTED_MERGE_COUNT.get()); 354 355 String classes; 356 StringBuilder builder = new StringBuilder(); 357 builder.append("Merging the following page classloader contexts into one:\n"); 358 for (PageClassLoaderContext context : contextDependencies) 359 { 360 classes = context.getClassNames().stream() 361 .map(this::getContextName) 362 .sorted() 363 .collect(Collectors.joining(", ")); 364 builder.append(String.format("\t%s (parent %s) (%s)\n", context.getName(), context.getParent().getName(), classes)); 365 } 366 LOGGER.debug(builder.toString().trim()); 367 } 368 369 Set<PageClassLoaderContext> allContextsIncludingDescendents = new HashSet<>(); 370 for (PageClassLoaderContext context : contextDependencies) 371 { 372 allContextsIncludingDescendents.add(context); 373 allContextsIncludingDescendents.addAll(context.getDescendents()); 374 } 375 376 PageClassLoaderContext merged; 377 378 // Collect the classes in these dependencies, then invalidate the contexts 379 380 Set<PageClassLoaderContext> furtherDependencies = new HashSet<>(); 381 382 Set<String> classNames = new HashSet<>(); 383 384 for (PageClassLoaderContext context : contextDependencies) 385 { 386 if (!context.isRoot()) 387 { 388 classNames.addAll(context.getClassNames()); 389 } 390 final PageClassLoaderContext parent = context.getParent(); 391 // We don't want the merged context to have a further dependency on 392 // the root context (it's not mergeable) nor on itself. 393 if (!parent.isRoot() && 394 !allContextsIncludingDescendents.contains(parent)) 395 { 396 furtherDependencies.add(parent); 397 } 398 } 399 400 final List<PageClassLoaderContext> contextsToInvalidate = contextDependencies.stream() 401 .filter(c -> !c.isRoot()) 402 .collect(Collectors.toList()); 403 404 if (!contextsToInvalidate.isEmpty()) 405 { 406 classesToInvalidate.addAll(invalidate(contextsToInvalidate.toArray(new PageClassLoaderContext[contextsToInvalidate.size()]))); 407 } 408 409 PageClassLoaderContext parent; 410 411 // No context dependencies, so parent is going to be the root one 412 if (furtherDependencies.size() == 0) 413 { 414 parent = root; 415 } 416 else 417 { 418 // Single shared context dependency, so it's our parent 419 if (furtherDependencies.size() == 1) 420 { 421 parent = furtherDependencies.iterator().next(); 422 } 423 // No single context dependency, so we'll need to recursively merge it 424 // so we can have a single parent. 425 else 426 { 427 parent = merge(furtherDependencies, plasticProxyFactoryProvider, root, classesToInvalidate); 428 LOGGER.debug("New context: {}", parent); 429 } 430 } 431 432 merged = new PageClassLoaderContext( 433 "merged " + MERGED_COUNTER.getAndIncrement(), 434 parent, 435 classNames, 436 plasticProxyFactoryProvider.apply(parent.getClassLoader()), 437 this::get); 438 439 parent.addChild(merged); 440 441// for (String className : classNames) 442// { 443// loadClass(className, merged); 444// } 445 446 NESTED_MERGE_COUNT.set(NESTED_MERGE_COUNT.get() - 1); 447 if (LOGGER.isDebugEnabled()) 448 { 449 LOGGER.debug("Nested merge count going down to {}", NESTED_MERGE_COUNT.get()); 450 } 451 452 return merged; 453 } 454 455 @Override 456 public void clear(String className) 457 { 458 final PageClassLoaderContext context = root.findByClassName(className); 459 if (context != null) 460 { 461// invalidationHub.fireInvalidationEvent(new ArrayList<>(invalidate(context))); 462 invalidate(context); 463 } 464 } 465 466 private String getContextName(String className) 467 { 468 String contextName = componentClassResolver.getLogicalName(className); 469 if (contextName == null) 470 { 471 contextName = className; 472 } 473 return contextName; 474 } 475 476 @Override 477 public Set<String> invalidate(PageClassLoaderContext ... contexts) 478 { 479 Set<String> classNames = new HashSet<>(); 480 for (PageClassLoaderContext context : contexts) { 481 addClassNames(context, classNames); 482 context.invalidate(); 483 if (context.getParent() != null) 484 { 485 context.getParent().removeChild(context); 486 } 487 } 488 return classNames; 489 } 490 491 private List<String> listen(List<String> resources) 492 { 493 494 List<String> returnValue; 495 496 if (!multipleClassLoaders) 497 { 498 for (PageClassLoaderContext context : root.getChildren()) 499 { 500 context.invalidate(); 501 } 502 returnValue = Collections.emptyList(); 503 } 504 else if (INVALIDATING_CONTEXT.get()) 505 { 506 returnValue = Collections.emptyList(); 507 } 508 else 509 { 510 511 Set<PageClassLoaderContext> contextsToInvalidate = new HashSet<>(); 512 for (String resource : resources) 513 { 514 PageClassLoaderContext context = root.findByClassName(resource); 515 if (context != null && !context.isRoot()) 516 { 517 contextsToInvalidate.add(context); 518 } 519 } 520 521 Set<String> furtherResources = invalidate(contextsToInvalidate.toArray( 522 new PageClassLoaderContext[contextsToInvalidate.size()])); 523 524 // We don't want to invalidate resources more than once 525 furtherResources.removeAll(resources); 526 527 returnValue = new ArrayList<>(furtherResources); 528 } 529 530 return returnValue; 531 532 } 533 534 @SuppressWarnings("unchecked") 535 @Override 536 public void invalidateAndFireInvalidationEvents(PageClassLoaderContext... contexts) { 537 markAsInvalidatingContext(); 538 if (multipleClassLoaders) 539 { 540 final Set<String> classNames = invalidate(contexts); 541 invalidate(classNames); 542 } 543 else 544 { 545 invalidate(Collections.EMPTY_SET); 546 } 547 markAsNotInvalidatingContext(); 548 } 549 550 private void markAsNotInvalidatingContext() { 551 INVALIDATING_CONTEXT.set(false); 552 } 553 554 private void markAsInvalidatingContext() { 555 INVALIDATING_CONTEXT.set(true); 556 } 557 558 private void invalidate(Set<String> classesToInvalidate) { 559 if (!classesToInvalidate.isEmpty()) 560 { 561 LOGGER.debug("Invalidating classes {}", classesToInvalidate); 562 markAsInvalidatingContext(); 563 final List<String> classesToInvalidateAsList = new ArrayList<>(classesToInvalidate); 564 565 componentDependencyRegistry.disableInvalidations(); 566 567 try 568 { 569 // TODO: do we really need both invalidation hubs to be invoked here? 570 invalidationHub.fireInvalidationEvent(classesToInvalidateAsList); 571 componentClassesInvalidationEventHub.fireInvalidationEvent(classesToInvalidateAsList); 572 markAsNotInvalidatingContext(); 573 } 574 finally 575 { 576 componentDependencyRegistry.enableInvalidations(); 577 } 578 579 } 580 } 581 582 private void addClassNames(PageClassLoaderContext context, Set<String> classNames) { 583 classNames.addAll(context.getClassNames()); 584 for (PageClassLoaderContext child : context.getChildren()) { 585 addClassNames(child, classNames); 586 } 587 } 588 589 @Override 590 public PageClassLoaderContext getRoot() { 591 return root; 592 } 593 594 @Override 595 public boolean isMerging() 596 { 597 return NESTED_MERGE_COUNT.get() > 0; 598 } 599 600 @Override 601 public void clear() 602 { 603 } 604 605 @Override 606 public Class<?> getClassInstance(Class<?> clasz, String pageName) 607 { 608 final String className = clasz.getName(); 609 PageClassLoaderContext context = root.findByClassName(className); 610 if (context == null) 611 { 612 context = get(className); 613 } 614 try 615 { 616 clasz = context.getProxyFactory().getClassLoader().loadClass(className); 617 } catch (ClassNotFoundException e) 618 { 619 throw new TapestryException(e.getMessage(), e); 620 } 621 return clasz; 622 } 623 624 @Override 625 public void preload() 626 { 627 628 final PageClassLoaderContext context = new PageClassLoaderContext(PageClassLoaderContext.UNKOWN_CONTEXT_NAME, root, 629 Collections.emptySet(), 630 plasticProxyFactoryProvider.apply(root.getClassLoader()), 631 this::get); 632 633 final List<String> pageNames = componentClassResolver.getPageNames(); 634 final List<String> classNames = new ArrayList<>(pageNames.size()); 635 636 long start = System.currentTimeMillis(); 637 638 LOGGER.info("Preloading dependency information for {} pages", pageNames.size()); 639 640 for (String page : pageNames) 641 { 642 try 643 { 644 final String className = componentClassResolver.resolvePageNameToClassName(page); 645 componentDependencyRegistry.register(context.getClassLoader().loadClass(className)); 646 classNames.add(className); 647 } catch (ClassNotFoundException e) 648 { 649 throw new RuntimeException(e); 650 } 651 } 652 653 long finish = System.currentTimeMillis(); 654 655 if (LOGGER.isInfoEnabled()) 656 { 657 LOGGER.info(String.format("Dependency information gathered in %.3f ms", (finish - start) / 1000.0)); 658 } 659 660 context.invalidate(); 661 662 LOGGER.info("Starting preloading page classloader contexts"); 663 664 start = System.currentTimeMillis(); 665 666 for (int i = 0; i < 10; i++) 667 { 668 for (String className : classNames) 669 { 670 get(className); 671 } 672 } 673 674 finish = System.currentTimeMillis(); 675 676 if (LOGGER.isInfoEnabled()) 677 { 678 LOGGER.info(String.format("Preloading of page classloadercontexts finished in %.3f ms", (finish - start) / 1000.0)); 679 } 680 681 } 682 683}