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