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}