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}