    001// Copyright 2022, 2023, 2024 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.internal.services;
    015
    016import java.io.BufferedReader;
    017import java.io.BufferedWriter;
    018import java.io.File;
    019import java.io.FileReader;
    020import java.io.FileWriter;
    021import java.io.IOException;
    022import java.lang.reflect.Field;
    023import java.util.ArrayList;
    024import java.util.Collection;
    025import java.util.Collections;
    026import java.util.HashMap;
    027import java.util.HashSet;
    028import java.util.Iterator;
    029import java.util.LinkedList;
    030import java.util.List;
    031import java.util.Locale;
    032import java.util.Map;
    033import java.util.Objects;
    034import java.util.Set;
    035import java.util.WeakHashMap;
    036import java.util.function.Consumer;
    037import java.util.stream.Collectors;
    038
    039import org.apache.tapestry5.ComponentResources;
    040import org.apache.tapestry5.SymbolConstants;
    041import org.apache.tapestry5.annotations.InjectComponent;
    042import org.apache.tapestry5.annotations.InjectPage;
    043import org.apache.tapestry5.annotations.Mixin;
    044import org.apache.tapestry5.annotations.MixinClasses;
    045import org.apache.tapestry5.annotations.Mixins;
    046import org.apache.tapestry5.commons.Resource;
    047import org.apache.tapestry5.commons.internal.util.TapestryException;
    048import org.apache.tapestry5.commons.services.InvalidationEventHub;
    049import org.apache.tapestry5.commons.util.UnknownValueException;
    050import org.apache.tapestry5.internal.TapestryInternalUtils;
    051import org.apache.tapestry5.internal.ThrowawayClassLoader;
    052import org.apache.tapestry5.internal.parser.ComponentTemplate;
    053import org.apache.tapestry5.internal.parser.StartComponentToken;
    054import org.apache.tapestry5.internal.parser.TemplateToken;
    055import org.apache.tapestry5.internal.structure.ComponentPageElement;
    056import org.apache.tapestry5.ioc.Orderable;
    057import org.apache.tapestry5.ioc.annotations.Symbol;
    058import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
    059import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    060import org.apache.tapestry5.ioc.services.PerthreadManager;
    061import org.apache.tapestry5.json.JSONArray;
    062import org.apache.tapestry5.json.JSONObject;
    063import org.apache.tapestry5.model.ComponentModel;
    064import org.apache.tapestry5.model.EmbeddedComponentModel;
    065import org.apache.tapestry5.model.MutableComponentModel;
    066import org.apache.tapestry5.model.ParameterModel;
    067import org.apache.tapestry5.plastic.PlasticField;
    068import org.apache.tapestry5.plastic.PlasticManager;
    069import org.apache.tapestry5.runtime.Component;
    070import org.apache.tapestry5.services.ComponentClassResolver;
    071import org.apache.tapestry5.services.pageload.PageClassLoaderContextManager;
    072import org.apache.tapestry5.services.templates.ComponentTemplateLocator;
    073import org.slf4j.Logger;
    074import org.slf4j.LoggerFactory;
    075
    076@SuppressWarnings("deprecation")
    077public class ComponentDependencyRegistryImpl implements ComponentDependencyRegistry
    078{
    079
    080    private static final List<String> EMPTY_LIST = Collections.emptyList();
    081
    082    final private PageClassLoaderContextManager pageClassLoaderContextManager;
    083
    084    private static final String META_ATTRIBUTE = "injectedComponentDependencies";
    085
    086    private static final String META_ATTRIBUTE_SEPARATOR = ",";
    087
    088    private static final String NO_DEPENDENCY = "NONE";
    089
    090    // Key is a component, values are the components that depend on it.
    091    final private Map<String, Set<Dependency>> map;
    092
    093    // Cache to check which classes were already processed or not.
    094    final private Set<String> alreadyProcessed;
    095
    096    final private File storedDependencies;
    097
    098    final private static ThreadLocal<Integer> INVALIDATIONS_DISABLED = ThreadLocal.withInitial(() -> 0);
    099
    100    final private PlasticManager plasticManager;
    101
    102    final private ComponentClassResolver resolver;
    103
    104    final private TemplateParser templateParser;
    105
    106    final private Map<String, Boolean> isPageCache = new WeakHashMap<>();
    107
    108    final private ComponentTemplateLocator componentTemplateLocator;
    109
    110    final private boolean storedDependencyInformationPresent;
    111
    112    private boolean enableEnsureClassIsAlreadyProcessed = true;
    113
    114    public ComponentDependencyRegistryImpl(
    115            final PageClassLoaderContextManager pageClassLoaderContextManager,
    116            final PlasticManager plasticManager,
    117            final ComponentClassResolver componentClassResolver,
    118            final TemplateParser templateParser,
    119            final ComponentTemplateLocator componentTemplateLocator,
    120            final @Symbol(SymbolConstants.COMPONENT_DEPENDENCY_FILE) String componentDependencyFile,
    121            final @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode)
    122    {
    123        this.pageClassLoaderContextManager = pageClassLoaderContextManager;
    124        map = new HashMap<>();
    125        alreadyProcessed = new HashSet<>();
    126        this.plasticManager = plasticManager;
    127        this.resolver = componentClassResolver;
    128        this.templateParser = templateParser;
    129        this.componentTemplateLocator = componentTemplateLocator;
    130
    131        if (!productionMode)
    132        {
    133
    134            Logger logger = LoggerFactory.getLogger(ComponentDependencyRegistry.class);
    135
    136            storedDependencies = new File(componentDependencyFile);
    137            final boolean fileExists = storedDependencies.exists();
    138
    139            logger.info("Component dependencies file: {} Found? {}",
    140                    storedDependencies.getAbsolutePath(), fileExists);
    141
    142            if (fileExists)
    143            {
    144                try (FileReader fileReader = new FileReader(storedDependencies);
    145                        BufferedReader reader = new BufferedReader(fileReader))
    146                {
    147                    StringBuilder builder = new StringBuilder();
    148                    String line = reader.readLine();
    149                    while (line != null)
    150                    {
    151                        builder.append(line);
    152                        line = reader.readLine();
    153                    }
    154                    JSONArray jsonArray = new JSONArray(builder.toString());
    155                    for (int i = 0; i < jsonArray.size(); i++)
    156                    {
    157                        final JSONObject jsonObject = jsonArray.getJSONObject(i);
    158                        final String className = jsonObject.getString("class");
    159                        final String type = jsonObject.getString("type");
    160                        if (!type.equals(NO_DEPENDENCY))
    161                        {
    162                            final DependencyType dependencyType = DependencyType.valueOf(type);
    163                            final String dependency = jsonObject.getString("dependency");
    164                            add(className, dependency, dependencyType);
    165                            alreadyProcessed.add(dependency);
    166                        }
    167                        alreadyProcessed.add(className);
    168                    }
    169                } catch (IOException e)
    170                {
    171                    throw new TapestryException("Exception trying to read " + storedDependencies.getAbsolutePath(), e);
    172                }
    173
    174            }
    175
    176        }
    177        else
    178        {
    179            storedDependencies = null;
    180        }
    181
    182        storedDependencyInformationPresent = !map.isEmpty();
    183
    184    }
    185
    186    public void setupThreadCleanup(final PerthreadManager perthreadManager)
    187    {
    188        perthreadManager.addThreadCleanupCallback(() -> {
    189            INVALIDATIONS_DISABLED.set(0);
    190        });
    191    }
    192
    193    @Override
    194    public void register(Class<?> component)
    195    {
    196        register(component, component.getClassLoader());
    197    }
    198
    199    @Override
    200    public void register(Class<?> component, ClassLoader classLoader)
    201    {
    202
    203        final String className = component.getName();
    204        final Set<Class<?>> furtherDependencies = new HashSet<>();
    205        Consumer<Class<?>> processClass = furtherDependencies::add;
    206        Consumer<String> processClassName = s -> {
    207            try {
    208                furtherDependencies.add(classLoader.loadClass(s));
    209            } catch (ClassNotFoundException e) {
    210                throw new RuntimeException(e);
    211            }
    212        };
    213
    214        // Components declared in the template
    215        registerTemplate(component, processClassName);
    216
    217        // Dependencies from injecting or component-declaring annotations:
    218        // @InjectPage, @InjectComponent
    219        for (Field field : component.getDeclaredFields())
    220        {
    221
    222            // Component injection annotation
    223            if (field.isAnnotationPresent(InjectComponent.class))
    224            {
    225                final Class<?> dependency = field.getType();
    226                add(component, dependency, DependencyType.USAGE);
    227                processClass.accept(dependency);
    228            }
    229
    230            // Page injection annotation
    231            if (field.isAnnotationPresent(InjectPage.class))
    232            {
    233                final Class<?> dependency = field.getType();
    234                add(component, dependency, DependencyType.INJECT_PAGE);
    235                processClass.accept(dependency);
    236            }
    237
    238            // @Component
    239            registerComponentInstance(field, processClassName);
    240
    241            // Mixins, class level: @Mixin
    242            registerMixin(field, processClassName);
    243
    244            // Mixins applied to embedded component instances through @MixinClasses or @Mixins
    245            registerComponentInstanceMixins(field, processClass, processClassName);
    246        }
    247
    248        // Superclass
    249        Class<?> superclass = component.getSuperclass();
    250        if (isTransformed(superclass))
    251        {
    252            processClass.accept(superclass);
    253            add(component, superclass, DependencyType.SUPERCLASS);
    254        }
    255
    256        alreadyProcessed.add(className);
    257
    258        for (Class<?> dependency : furtherDependencies)
    259        {
    260            // Avoid infinite recursion
    261            final String dependencyClassName = dependency.getName();
    262            if (!alreadyProcessed.contains(dependencyClassName)
    263                    && plasticManager.shouldInterceptClassLoading(dependency.getName()))
    264            {
    265                register(dependency, classLoader);
    266            }
    267        }
    268
    269    }
    270
    271    /**
    272     * Notice only the main template (i.e. not the locale- or axis-specific ones)
    273     * are checked here. They hopefully will be covered when the ComponentModel-based
    274     * component dependency processing is done.
    275     * @param component
    276     * @param processClassName
    277     */
    278    private void registerTemplate(Class<?> component, Consumer<String> processClassName)
    279    {
    280        // TODO: implement caching of template dependency information, probably
    281        // by listening separately to ComponentTemplateSource to invalidate caches
    282        // just when template changes.
    283
    284        final String className = component.getName();
    285        ComponentModel mock = new ComponentModelMock(component, isPage(className));
    286        final Resource templateResource = componentTemplateLocator.locateTemplate(mock, Locale.getDefault());
    287        String dependency;
    288        if (templateResource != null && templateResource.exists())
    289        {
    290            final ComponentTemplate template = templateParser.parseTemplate(templateResource);
    291            final List<TemplateToken> tokens = new LinkedList<>();
    292
    293            tokens.addAll(template.getTokens());
    294            for (String id : template.getExtensionPointIds())
    295            {
    296                tokens.addAll(template.getExtensionPointTokens(id));
    297            }
    298
    299            for (TemplateToken token : tokens)
    300            {
    301                if (token instanceof StartComponentToken)
    302                {
    303                    StartComponentToken componentToken = (StartComponentToken) token;
    304                    String logicalName = componentToken.getComponentType();
    305                    if (logicalName != null)
    306                    {
    307                        try
    308                        {
    309                            dependency = resolver.resolveComponentTypeToClassName(logicalName);
    310                            add(className, dependency, DependencyType.USAGE);
    311                            processClassName.accept(dependency);
    312                        }
    313                        catch (UnknownValueException e)
    314                        {
    315                            // Logical name doesn't match an existing component. Ignore
    316                        }
    317                    }
    318                    for (String mixin : TapestryInternalUtils.splitAtCommas(componentToken.getMixins()))
    319                    {
    320                        try
    321                        {
    322                            if (mixin.contains("::"))
    323                            {
    324                                mixin = mixin.substring(0, mixin.indexOf("::"));
    325                            }
    326                            dependency = resolver.resolveMixinTypeToClassName(mixin);
    327                            add(className, dependency, DependencyType.USAGE);
    328                            processClassName.accept(dependency);
    329                        }
    330                        catch (UnknownValueException e)
    331                        {
    332                            // Mixin name doesn't match an existing mixin. Ignore
    333                        }
    334
    335                    }
    336                }
    337            }
    338        }
    339    }
    340
    341    private boolean isPage(final String className)
    342    {
    343        Boolean result = isPageCache.get(className);
    344        if (result == null)
    345        {
    346            result = resolver.isPage(className);
    347            isPageCache.put(className, result);
    348        }
    349        return result;
    350    }
    351
    352    private void registerComponentInstance(Field field, Consumer<String> processClassName)
    353    {
    354        if (field.isAnnotationPresent(org.apache.tapestry5.annotations.Component.class))
    355        {
    356            org.apache.tapestry5.annotations.Component component =
    357                    field.getAnnotation(org.apache.tapestry5.annotations.Component.class);
    358
    359            final String typeFromAnnotation = component.type().trim();
    360            String dependency;
    361            if (typeFromAnnotation.isEmpty())
    362            {
    363                dependency = field.getType().getName();
    364            }
    365            else
    366            {
    367                dependency = resolver.resolveComponentTypeToClassName(typeFromAnnotation);
    368            }
    369            add(field.getDeclaringClass().getName(), dependency, DependencyType.USAGE);
    370            processClassName.accept(dependency);
    371        }
    372    }
    373
    374    private void registerMixin(Field field, Consumer<String> processClassName) {
    375        if (field.isAnnotationPresent(Mixin.class))
    376        {
    377            // Logic adapted from MixinWorker
    378            String mixinType = field.getAnnotation(Mixin.class).value();
    379            String mixinClassName = InternalUtils.isBlank(mixinType) ?
    380                    getFieldTypeClassName(field) :
    381                    resolver.resolveMixinTypeToClassName(mixinType);
    382
    383            add(getDeclaringClassName(field), mixinClassName, DependencyType.USAGE);
    384            processClassName.accept(mixinClassName);
    385        }
    386    }
    387
    388    private String getDeclaringClassName(Field field) {
    389        return field.getDeclaringClass().getName();
    390    }
    391
    392    private String getFieldTypeClassName(Field field) {
    393        return field.getType().getName();
    394    }
    395
    396    private void registerComponentInstanceMixins(Field field, Consumer<Class<?>> processClass, Consumer<String> processClassName)
    397    {
    398
    399        if (field.isAnnotationPresent(org.apache.tapestry5.annotations.Component.class))
    400        {
    401
    402            MixinClasses mixinClasses = field.getAnnotation(MixinClasses.class);
    403            if (mixinClasses != null)
    404            {
    405                for (Class<?> dependency : mixinClasses.value())
    406                {
    407                    add(field.getDeclaringClass(), dependency, DependencyType.USAGE);
    408                    processClass.accept(dependency);
    409                }
    410            }
    411
    412            Mixins mixins = field.getAnnotation(Mixins.class);
    413            if (mixins != null)
    414            {
    415                for (String mixin : mixins.value())
    416                {
    417                    // Logic adapted from MixinsWorker
    418                    Orderable<String> typeAndOrder = TapestryInternalUtils.mixinTypeAndOrder(mixin);
    419                    final String dependency = resolver.resolveMixinTypeToClassName(typeAndOrder.getTarget());
    420                    add(getDeclaringClassName(field), dependency, DependencyType.USAGE);
    421                    processClassName.accept(dependency);
    422                }
    423            }
    424
    425        }
    426
    427    }
    428
    429    @Override
    430    public void register(ComponentPageElement componentPageElement)
    431    {
    432        final String componentClassName = getClassName(componentPageElement);
    433
    434        if (!alreadyProcessed.contains(componentClassName))
    435        {
    436            synchronized (map)
    437            {
    438
    439                // Components in the tree (i.e. declared in the template
    440                for (String id : componentPageElement.getEmbeddedElementIds())
    441                {
    442                    final ComponentPageElement child = componentPageElement.getEmbeddedElement(id);
    443                    add(componentPageElement, child, DependencyType.USAGE);
    444                    register(child);
    445                }
    446
    447                // Mixins, class level
    448                final ComponentResources componentResources = componentPageElement.getComponentResources();
    449                final ComponentModel componentModel = componentResources.getComponentModel();
    450                for (String mixinClassName : componentModel.getMixinClassNames())
    451                {
    452                    add(componentClassName, mixinClassName, DependencyType.USAGE);
    453                }
    454
    455                // Mixins applied to embedded component instances
    456                final List<String> embeddedComponentIds = componentModel.getEmbeddedComponentIds();
    457                for (String id : embeddedComponentIds)
    458                {
    459                    final EmbeddedComponentModel embeddedComponentModel = componentResources
    460                            .getComponentModel()
    461                            .getEmbeddedComponentModel(id);
    462                    final List<String> mixinClassNames = embeddedComponentModel
    463                            .getMixinClassNames();
    464                    for (String mixinClassName : mixinClassNames) {
    465                        add(componentClassName, mixinClassName, DependencyType.USAGE);
    466                    }
    467                }
    468
    469                // Superclass
    470                final Component component = componentPageElement.getComponent();
    471                Class<?> parent = component.getClass().getSuperclass();
    472                if (parent != null && !Object.class.equals(parent))
    473                {
    474                    add(componentClassName, parent.getName(), DependencyType.SUPERCLASS);
    475                }
    476
    477                // Dependencies from injecting annotations:
    478                // @InjectPage, @InjectComponent, @InjectComponent
    479                final String metaDependencies = component.getComponentResources().getComponentModel().getMeta(META_ATTRIBUTE);
    480                if (metaDependencies != null)
    481                {
    482                    for (String dependency : metaDependencies.split(META_ATTRIBUTE_SEPARATOR))
    483                    {
    484                        add(componentClassName, dependency,
    485                                isPage(dependency) ? DependencyType.INJECT_PAGE : DependencyType.USAGE);
    486                    }
    487                }
    488
    489                alreadyProcessed.add(componentClassName);
    490
    491            }
    492
    493        }
    494
    495    }
    496
    497    @Override
    498    public void register(PlasticField plasticField, MutableComponentModel componentModel)
    499    {
    500        if (plasticField.hasAnnotation(InjectPage.class) ||
    501                plasticField.hasAnnotation(InjectComponent.class) ||
    502                plasticField.hasAnnotation(org.apache.tapestry5.annotations.Component.class))
    503        {
    504            String dependencies = componentModel.getMeta(META_ATTRIBUTE);
    505            final String dependency = plasticField.getTypeName();
    506            if (dependencies == null)
    507            {
    508                dependencies = dependency;
    509            }
    510            else
    511            {
    512                if (!dependencies.contains(dependency))
    513                {
    514                    dependencies = dependencies + META_ATTRIBUTE_SEPARATOR + dependency;
    515                }
    516            }
    517            componentModel.setMeta(META_ATTRIBUTE, dependencies);
    518        }
    519    }
    520
    521    private String getClassName(ComponentPageElement component)
    522    {
    523        return component.getComponentResources().getComponentModel().getComponentClassName();
    524    }
    525
    526    @Override
    527    public void clear(String className)
    528    {
    529        synchronized (map)
    530        {
    531            alreadyProcessed.remove(className);
    532            map.remove(className);
    533            final Collection<Set<Dependency>> allDependentSets = map.values();
    534            for (Set<Dependency> dependents : allDependentSets)
    535            {
    536                if (dependents != null)
    537                {
    538                    final Iterator<Dependency> iterator = dependents.iterator();
    539                    while (iterator.hasNext())
    540                    {
    541                        if (className.equals(iterator.next().className))
    542                        {
    543                            iterator.remove();
    544                        }
    545                    }
    546                }
    547            }
    548        }
    549    }
    550
    551    @Override
    552    public void clear(ComponentPageElement component)
    553    {
    554        clear(getClassName(component));
    555    }
    556
    557    @Override
    558    public void clear() {
    559        map.clear();
    560        alreadyProcessed.clear();
    561    }
    562
    563    @Override
    564    public Set<String> getDependents(String className)
    565    {
    566
    567        ensureClassIsAlreadyProcessed(className);
    568
    569        final Set<Dependency> dependents = map.get(className);
    570        return dependents != null
    571                ? dependents.stream().map(d -> d.className).collect(Collectors.toSet())
    572                : Collections.emptySet();
    573    }
    574
    575    @Override
    576    public Set<String> getDependencies(String className, DependencyType type)
    577    {
    578
    579        ensureClassIsAlreadyProcessed(className);
    580
    581        Set<String> dependencies = Collections.emptySet();
    582        if (alreadyProcessed.contains(className))
    583        {
    584            dependencies = map.entrySet().stream()
    585                .filter(e -> contains(e.getValue(), className, type))
    586                .map(e -> e.getKey())
    587                .collect(Collectors.toSet());
    588        }
    589
    590        return dependencies;
    591    }
    592
    593    @Override
    594    public Set<String> getAllNonPageDependencies(String className)
    595    {
    596        final Set<String> dependencies = new HashSet<>();
    597        getAllNonPageDependencies(className, dependencies);
    598        // Just in case, since it's possible to have circular dependencies.
    599        dependencies.remove(className);
    600        return Collections.unmodifiableSet(dependencies);
    601    }
    602
    603    private void getAllNonPageDependencies(String className, Set<String> dependencies)
    604    {
    605        Set<String> theseDependencies = new HashSet<>();
    606        theseDependencies.addAll(getDependencies(className, DependencyType.USAGE));
    607        theseDependencies.addAll(getDependencies(className, DependencyType.SUPERCLASS));
    608        theseDependencies.removeAll(dependencies);
    609        dependencies.addAll(theseDependencies);
    610        for (String dependency : theseDependencies)
    611        {
    612            getAllNonPageDependencies(dependency, dependencies);
    613        }
    614    }
    615
    616
    617    private boolean contains(Set<Dependency> dependencies, String className, DependencyType type)
    618    {
    619        boolean contains = false;
    620        for (Dependency dependency : dependencies)
    621        {
    622            if (dependency.type.equals(type) && dependency.className.equals(className))
    623            {
    624                contains = true;
    625                break;
    626            }
    627        }
    628        return contains;
    629    }
    630
    631    private void add(ComponentPageElement component, ComponentPageElement dependency, DependencyType type)
    632    {
    633        add(getClassName(component), getClassName(dependency), type);
    634    }
    635
    636    // Just for unit tests
    637    void add(String component, String dependency, DependencyType type, boolean markAsAlreadyProcessed)
    638    {
    639        if (markAsAlreadyProcessed)
    640        {
    641            alreadyProcessed.add(component);
    642        }
    643        if (dependency != null)
    644        {
    645            add(component, dependency, type);
    646        }
    647    }
    648
    649    private void add(Class<?> component, Class<?> dependency, DependencyType type)
    650    {
    651        if (plasticManager.shouldInterceptClassLoading(dependency.getName()))
    652        {
    653            add(component.getName(), dependency.getName(), type);
    654        }
    655    }
    656
    657    private void add(String component, String dependency, DependencyType type)
    658    {
    659        Objects.requireNonNull(component, "Parameter component cannot be null");
    660        Objects.requireNonNull(dependency, "Parameter dependency cannot be null");
    661        Objects.requireNonNull(dependency, "Parameter type cannot be null");
    662        synchronized (map)
    663        {
    664            if (!component.equals(dependency))
    665            {
    666                Set<Dependency> dependents = map.get(dependency);
    667                if (dependents == null)
    668                {
    669                    dependents = new HashSet<>();
    670                    map.put(dependency, dependents);
    671                }
    672                dependents.add(new Dependency(component, type));
    673            }
    674        }
    675    }
    676
    677    @Override
    678    public void listen(InvalidationEventHub invalidationEventHub)
    679    {
    680        invalidationEventHub.addInvalidationCallback(this::listen);
    681    }
    682
    683    // Protected just for testing
    684    List<String> listen(List<String> resources)
    685    {
    686        List<String> furtherDependents = EMPTY_LIST;
    687        if (resources.isEmpty())
    688        {
    689            clear();
    690            furtherDependents = EMPTY_LIST;
    691        }
    692        else if (INVALIDATIONS_DISABLED.get() > 0)
    693        {
    694            furtherDependents = Collections.emptyList();
    695        }
    696        // Don't invalidate component dependency information when
    697        // PageClassloaderContextManager is merging contexts
    698        // TODO: is this still needed since the inception of INVALIDATIONS_ENABLED?
    699        else if (!pageClassLoaderContextManager.isMerging())
    700        {
    701            furtherDependents = new ArrayList<>();
    702            for (String resource : resources)
    703            {
    704
    705                final Set<String> dependents = getDependents(resource);
    706                for (String furtherDependent : dependents)
    707                {
    708                    if (!resources.contains(furtherDependent) && !furtherDependents.contains(furtherDependent))
    709                    {
    710                        furtherDependents.add(furtherDependent);
    711                    }
    712                }
    713
    714                clear(resource);
    715
    716            }
    717        }
    718        return furtherDependents;
    719    }
    720
    721    @Override
    722    public void writeFile()
    723    {
    724        synchronized (this)
    725        {
    726            try (FileWriter fileWriter = new FileWriter(storedDependencies);
    727                    BufferedWriter bufferedWriter = new BufferedWriter(fileWriter))
    728            {
    729                Set<String> classNames = new HashSet<>(alreadyProcessed.size());
    730                classNames.addAll(map.keySet());
    731                classNames.addAll(alreadyProcessed);
    732                JSONArray jsonArray = new JSONArray();
    733                for (String className : classNames)
    734                {
    735                    boolean hasDependencies = false;
    736                    for (DependencyType dependencyType : DependencyType.values())
    737                    {
    738                        final Set<String> dependencies = getDependencies(className, dependencyType);
    739                        for (String dependency : dependencies)
    740                        {
    741                            JSONObject object = new JSONObject();
    742                            object.put("class", className);
    743                            object.put("type", dependencyType.name());
    744                            object.put("dependency", dependency);
    745                            jsonArray.add(object);
    746                            hasDependencies = true;
    747                        }
    748                    }
    749                    // Add a fake dependency so classes without dependencies
    750                    // nor classes depending on it are properly stored and
    751                    // retrieved, thus avoiding these classes getting into the
    752                    // unknown page classloader context.
    753                    if (!hasDependencies)
    754                    {
    755                        if (getDependents(className).isEmpty()) {
    756                            JSONObject object = new JSONObject();
    757                            object.put("class", className);
    758                            object.put("type", NO_DEPENDENCY);
    759                            jsonArray.add(object);
    760                        }
    761                    }
    762                }
    763                bufferedWriter.write(jsonArray.toString());
    764            }
    765            catch (IOException e)
    766            {
    767                throw new TapestryException("Exception trying to write " + storedDependencies.getAbsolutePath(), e);
    768            }
    769
    770            Logger logger = LoggerFactory.getLogger(ComponentDependencyRegistry.class);
    771
    772            logger.info("Component dependencies written to {}",
    773                    storedDependencies.getAbsolutePath());
    774        }
    775    }
    776
    777    @Override
    778    public boolean contains(String className)
    779    {
    780        return alreadyProcessed.contains(className);
    781    }
    782
    783    @Override
    784    public Set<String> getClassNames()
    785    {
    786        return Collections.unmodifiableSet(new HashSet<>(alreadyProcessed));
    787    }
    788
    789    @Override
    790    public Set<String> getRootClasses() {
    791        return alreadyProcessed.stream()
    792                .filter(c -> getDependencies(c, DependencyType.USAGE).isEmpty() &&
    793                        getDependencies(c, DependencyType.INJECT_PAGE).isEmpty() &&
    794                        getDependencies(c, DependencyType.SUPERCLASS).isEmpty())
    795                .collect(Collectors.toSet());
    796    }
    797
    798    private boolean isTransformed(Class<?> clasz)
    799    {
    800        return plasticManager.shouldInterceptClassLoading(clasz.getName());
    801    }
    802
    803    @Override
    804    public boolean isStoredDependencyInformationPresent()
    805    {
    806        return storedDependencyInformationPresent;
    807    }
    808
    809    @Override
    810    public void disableInvalidations()
    811    {
    812        INVALIDATIONS_DISABLED.set(INVALIDATIONS_DISABLED.get() + 1);
    813    }
    814
    815    @Override
    816    public void enableInvalidations()
    817    {
    818        INVALIDATIONS_DISABLED.set(INVALIDATIONS_DISABLED.get() - 1);
    819        if (INVALIDATIONS_DISABLED.get() < 0)
    820        {
    821            INVALIDATIONS_DISABLED.set(0);
    822        }
    823    }
    824
    825    // Only for unit tests
    826    void setEnableEnsureClassIsAlreadyProcessed(boolean enableEnsureClassIsAlreadyProcessed) {
    827        this.enableEnsureClassIsAlreadyProcessed = enableEnsureClassIsAlreadyProcessed;
    828    }
    829
    830    private void ensureClassIsAlreadyProcessed(String className) {
    831        if (enableEnsureClassIsAlreadyProcessed && !contains(className))
    832        {
    833            ThrowawayClassLoader classLoader = new ThrowawayClassLoader(getClass().getClassLoader());
    834            try
    835            {
    836                register(classLoader.loadClass(className));
    837            } catch (ClassNotFoundException e)
    838            {
    839                throw new RuntimeException(e);
    840            }
    841        }
    842    }
    843
    844    /**
    845     * Only really implemented method is {@link ComponentModel#getBaseResource()}
    846     */
    847    private class ComponentModelMock implements ComponentModel
    848    {
    849
    850        final private Resource baseResource;
    851        final private boolean isPage;
    852        final private String componentClassName;
    853
    854        public ComponentModelMock(Class<?> component, boolean isPage)
    855        {
    856            componentClassName = component.getName();
    857            String templateLocation = componentClassName.replace('.', '/');
    858            baseResource = new ClasspathResource(templateLocation);
    859
    860            this.isPage = isPage;
    861        }
    862
    863        @Override
    864        public Resource getBaseResource()
    865        {
    866            return baseResource;
    867        }
    868
    869        @Override
    870        public String getLibraryName()
    871        {
    872            return null;
    873        }
    874
    875        @Override
    876        public boolean isPage()
    877        {
    878            return isPage;
    879        }
    880
    881        @Override
    882        public String getComponentClassName()
    883        {
    884            return componentClassName;
    885        }
    886
    887        @Override
    888        public List<String> getEmbeddedComponentIds()
    889        {
    890            return null;
    891        }
    892
    893        @Override
    894        public EmbeddedComponentModel getEmbeddedComponentModel(String componentId)
    895        {
    896            return null;
    897        }
    898
    899        @Override
    900        public String getFieldPersistenceStrategy(String fieldName)
    901        {
    902            return null;
    903        }
    904
    905        @Override
    906        public Logger getLogger()
    907        {
    908            return null;
    909        }
    910
    911        @Override
    912        public List<String> getMixinClassNames()
    913        {
    914            return null;
    915        }
    916
    917        @Override
    918        public ParameterModel getParameterModel(String parameterName)
    919        {
    920            return null;
    921        }
    922
    923        @Override
    924        public boolean isFormalParameter(String parameterName)
    925        {
    926            return false;
    927        }
    928
    929        @Override
    930        public List<String> getParameterNames()
    931        {
    932            return null;
    933        }
    934
    935        @Override
    936        public List<String> getDeclaredParameterNames()
    937        {
    938            return null;
    939        }
    940
    941        @Override
    942        public List<String> getPersistentFieldNames()
    943        {
    944            return null;
    945        }
    946
    947        @Override
    948        public boolean isRootClass()
    949        {
    950            return false;
    951        }
    952
    953        @Override
    954        public boolean getSupportsInformalParameters()
    955        {
    956            return false;
    957        }
    958
    959        @Override
    960        public ComponentModel getParentModel()
    961        {
    962            return null;
    963        }
    964
    965        @Override
    966        public boolean isMixinAfter()
    967        {
    968            return false;
    969        }
    970
    971        @Override
    972        public String getMeta(String key)
    973        {
    974            return null;
    975        }
    976
    977        @SuppressWarnings("rawtypes")
    978        @Override
    979        public Set<Class> getHandledRenderPhases()
    980        {
    981            return null;
    982        }
    983
    984        @Override
    985        public boolean handlesEvent(String eventType)
    986        {
    987            return false;
    988        }
    989
    990        @Override
    991        public String[] getOrderForMixin(String mixinClassName)
    992        {
    993            return null;
    994        }
    995
    996        @Override
    997        public boolean handleActivationEventContext()
    998        {
    999            return false;
    1000        }
    1001
    1002    }
    1003
    1004    private static final class Dependency
    1005    {
    1006        private final String className;
    1007        private final DependencyType type;
    1008
    1009        public Dependency(String className, DependencyType dependencyType)
    1010        {
    1011            super();
    1012            this.className = className;
    1013            this.type = dependencyType;
    1014        }
    1015
    1016        @Override
    1017        public int hashCode() {
    1018            return Objects.hash(className, type);
    1019        }
    1020
    1021        @Override
    1022        public boolean equals(Object obj)
    1023        {
    1024            if (this == obj)
    1025            {
    1026                return true;
    1027            }
    1028            if (!(obj instanceof Dependency))
    1029            {
    1030                return false;
    1031            }
    1032            Dependency other = (Dependency) obj;
    1033            return Objects.equals(className, other.className) && type == other.type;
    1034        }
    1035
    1036        @Override
    1037        public String toString()
    1038        {
    1039            return "Dependency [className=" + className + ", dependencyType=" + type + "]";
    1040        }
    1041
    1042    }
    1043
    1044}