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}