    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}