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.Objects; 020import java.util.Set; 021import java.util.function.Function; 022 023import org.apache.tapestry5.commons.services.PlasticProxyFactory; 024import org.apache.tapestry5.internal.plastic.PlasticClassLoader; 025import org.apache.tapestry5.plastic.PlasticManager; 026import org.apache.tapestry5.plastic.PlasticUtils; 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029 030/** 031 * Class that encapsulates a classloader context for Tapestry's live class reloading. 032 * Each instance contains basically a classloader, a set of classnames, a parent 033 * context (possibly null) and child contexts (possibly empty). 034 */ 035public class PageClassLoaderContext 036{ 037 038 private static final Logger LOGGER = LoggerFactory.getLogger(PageClassLoaderContext.class); 039 040 private final String name; 041 042 private final PageClassLoaderContext parent; 043 044 private final Set<String> classNames = new HashSet<>(); 045 046 private final Set<PageClassLoaderContext> children; 047 048 private final PlasticManager plasticManager; 049 050 private final PlasticProxyFactory proxyFactory; 051 052 private PageClassLoaderContext root; 053 054 private final Function<String, PageClassLoaderContext> provider; 055 056 /** 057 * Name of the <code>unknown</code> context (i.e. the one for controlled classes 058 * without dependency information at the moment). 059 */ 060 public static final String UNKOWN_CONTEXT_NAME = "unknown"; 061 062 public PageClassLoaderContext(String name, 063 PageClassLoaderContext parent, 064 Set<String> classNames, 065 PlasticProxyFactory plasticProxyFactory, 066 Function<String, PageClassLoaderContext> provider) 067 { 068 super(); 069 this.name = name; 070 this.parent = parent; 071 this.classNames.addAll(classNames); 072 this.plasticManager = plasticProxyFactory.getPlasticManager(); 073 this.proxyFactory = plasticProxyFactory; 074 this.provider = provider; 075 children = new HashSet<>(); 076 if (plasticProxyFactory.getClassLoader() instanceof PlasticClassLoader) 077 { 078 final PlasticClassLoader plasticClassLoader = (PlasticClassLoader) plasticManager.getClassLoader(); 079 plasticClassLoader.setTag(name); 080 plasticClassLoader.setFilter(this::filter); 081 plasticClassLoader.setAlternativeClassloading(this::alternativeClassLoading); 082 // getPlasticManager().getPool().setName(name); 083 if (parent != null) 084 { 085 getPlasticManager().getPool().setParent(parent.getPlasticManager().getPool()); 086 } 087 } 088 } 089 090 private Class<?> alternativeClassLoading(String className) 091 { 092 Class<?> clasz = null; 093 setRootFieldIfNeeded(); 094 PageClassLoaderContext context = root.findByClassName( 095 PlasticUtils.getEnclosingClassName(className)); 096 if (isRoot() && context == null) 097 { 098 context = this; 099 } 100 if (context != null) 101 { 102 try 103 { 104 final PlasticClassLoader classLoader = (PlasticClassLoader) context.getClassLoader(); 105 // Avoiding infinite recursion 106 synchronized (classLoader) 107 { 108 classLoader.setAlternativeClassloading(null); 109 clasz = classLoader.loadClass(className); 110 classLoader.setAlternativeClassloading(this::alternativeClassLoading); 111 } 112 } catch (ClassNotFoundException e) 113 { 114 throw new RuntimeException(e); 115 } 116 } 117 else if (root.getPlasticManager().shouldInterceptClassLoading(className)) 118 { 119 context = provider.apply(className); 120 } 121 return clasz; 122 } 123 124 private void setRootFieldIfNeeded() 125 { 126 if (root == null) 127 { 128 if (isRoot()) 129 { 130 root = this; 131 } 132 else 133 { 134 root = this; 135 while (!root.isRoot()) 136 { 137 root = root.getParent(); 138 } 139 } 140 } 141 } 142 143 /** 144 * Returns the name of this context. 145 */ 146 public String getName() 147 { 148 return name; 149 } 150 151 /** 152 * Returns the parent of this context. 153 */ 154 public PageClassLoaderContext getParent() 155 { 156 return parent; 157 } 158 159 /** 160 * Returns the set of classes that belong in this context. 161 */ 162 public Set<String> getClassNames() 163 { 164 return classNames; 165 } 166 167 /** 168 * Returns the children of this context. 169 */ 170 public Set<PageClassLoaderContext> getChildren() 171 { 172 return children; 173 } 174 175 /** 176 * Returns this context's {@linkplain PlasticManager} instance. 177 */ 178 public PlasticManager getPlasticManager() 179 { 180 return plasticManager; 181 } 182 183 /** 184 * Returns this context's {@linkplain PlasticProxyFactory} instance. 185 */ 186 public PlasticProxyFactory getProxyFactory() 187 { 188 return proxyFactory; 189 } 190 191 /** 192 * Adds a class to this context. 193 */ 194 public void addClass(String className) 195 { 196 classNames.add(className); 197 } 198 199 /** 200 * Adds a child context. 201 */ 202 public void addChild(PageClassLoaderContext context) 203 { 204 children.add(context); 205 } 206 207 /** 208 * Removes a child context. 209 */ 210 public void removeChild(PageClassLoaderContext context) 211 { 212 children.remove(context); 213 } 214 215 /** 216 * Searches for the context that contains the given class in itself and recursivel in its children. 217 */ 218 public PageClassLoaderContext findByClassName(String className) 219 { 220 PageClassLoaderContext context = null; 221 if (classNames.contains(className)) 222 { 223 context = this; 224 } 225 else 226 { 227 for (PageClassLoaderContext child : children) { 228 context = child.findByClassName(className); 229 if (context != null) 230 { 231 break; 232 } 233 } 234 } 235 return context; 236 } 237 238 /** 239 * Returns the {@linkplain ClassLoader} associated with this context. 240 */ 241 public ClassLoader getClassLoader() 242 { 243 return proxyFactory.getClassLoader(); 244 } 245 246 /** 247 * Invalidates this context and its children recursively. This shouldn't 248 * be called directly, just through {@link PageClassLoaderContextManager#invalidate(PageClassLoaderContext...)}. 249 */ 250 public void invalidate() 251 { 252 for (PageClassLoaderContext child : new ArrayList<>(children)) 253 { 254 child.invalidate(); 255 } 256 LOGGER.debug("Invalidating page classloader context '{}' (class loader {}, classes : {})", 257 name, proxyFactory.getClassLoader(), classNames); 258// classNames.clear(); 259 parent.getChildren().remove(this); 260 proxyFactory.clearCache(); 261 } 262 263 /** 264 * Returns whether this is the root context. 265 */ 266 public boolean isRoot() 267 { 268 return parent == null; 269 } 270 271 /** 272 * Returns whether this is the <code>unknwon</code> context. 273 * @see #UNKOWN_CONTEXT_NAME 274 */ 275 public boolean isUnknown() 276 { 277 return name.equals(UNKOWN_CONTEXT_NAME); 278 } 279 280 /** 281 * Returns the set of descendents (children and their children recursively 282 * of this context. 283 */ 284 public Set<PageClassLoaderContext> getDescendents() 285 { 286 Set<PageClassLoaderContext> descendents; 287 if (children.isEmpty()) 288 { 289 descendents = Collections.emptySet(); 290 } 291 else 292 { 293 descendents = new HashSet<>(children); 294 for (PageClassLoaderContext child : children) 295 { 296 descendents.addAll(child.getDescendents()); 297 } 298 } 299 return descendents; 300 } 301 302 @Override 303 public int hashCode() { 304 return Objects.hash(name); 305 } 306 307 @Override 308 public boolean equals(Object obj) { 309 if (this == obj) { 310 return true; 311 } 312 if (!(obj instanceof PageClassLoaderContext)) { 313 return false; 314 } 315 PageClassLoaderContext other = (PageClassLoaderContext) obj; 316 return Objects.equals(name, other.name); 317 } 318 319 @Override 320 public String toString() 321 { 322 return toString(true); 323 } 324 325 public String toString(boolean includeClassNames) 326 { 327 final PlasticClassLoader classLoader = (PlasticClassLoader) proxyFactory.getClassLoader(); 328 return "PageClassloaderContext [name=" + name + 329 ", parent=" + (parent != null ? parent.getName() : "null" ) + 330 ", classLoader=" + afterAt(classLoader.getClassLoaderId()) + 331 (isRoot() || !includeClassNames ? "" : ", classNames=" + classNames) + 332 "]"; 333 } 334 335 public String toRecursiveString() 336 { 337 return toRecursiveString(true); 338 } 339 340 public String toRecursiveString(boolean outputClasses) 341 { 342 StringBuilder builder = new StringBuilder(); 343 toRecursiveString(builder, "", outputClasses); 344 return builder.toString(); 345 } 346 347 public final boolean isEqualOrAncestor(PageClassLoaderContext dependencyContext) 348 { 349 boolean equalOrAncestor = this.equals(dependencyContext); 350 if (!equalOrAncestor) 351 { 352 PageClassLoaderContext parent = this.getParent(); 353 while (parent != null && !equalOrAncestor) 354 { 355 equalOrAncestor = parent.equals(dependencyContext); 356 if (equalOrAncestor) 357 { 358 break; 359 } 360 else 361 { 362 parent = parent.getParent(); 363 } 364 } 365 } 366 return equalOrAncestor; 367 } 368 369 private void toRecursiveString(StringBuilder builder, String tabs, boolean outputClasses) 370 { 371 builder.append(tabs); 372 builder.append(name); 373 builder.append(" : "); 374 builder.append(afterAt(proxyFactory.getClassLoader().toString())); 375 if (outputClasses) { 376 builder.append(" : "); 377 builder.append(classNames); 378 } 379 builder.append("\n"); 380 for (PageClassLoaderContext child : children) { 381 child.toRecursiveString(builder, tabs + "\t", outputClasses); 382 } 383 } 384 385 private static String afterAt(String string) 386 { 387 int index = string.indexOf('@'); 388 if (index > 0) 389 { 390 string = string.substring(index + 1); 391 } 392 return string; 393 } 394 395 private boolean filter(String className) 396 { 397 final int index = className.indexOf("$"); 398 if (index > 0) 399 { 400 className = className.substring(0, index); 401 } 402 // TODO: do we really need the className.contains(".base.") part? 403 return classNames.contains(className) || className.contains(".base.") || isUnknown(); 404 } 405 406}