    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}