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