    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.internal.services;
    014
    015import java.util.ArrayList;
    016import java.util.Collections;
    017import java.util.Iterator;
    018import java.util.List;
    019import java.util.Locale;
    020import java.util.Map;
    021import java.util.Map.Entry;
    022import java.util.Set;
    023import java.util.stream.Collectors;
    024
    025import org.apache.tapestry5.SymbolConstants;
    026import org.apache.tapestry5.TapestryConstants;
    027import org.apache.tapestry5.commons.Location;
    028import org.apache.tapestry5.commons.Resource;
    029import org.apache.tapestry5.commons.services.InvalidationEventHub;
    030import org.apache.tapestry5.commons.util.CollectionFactory;
    031import org.apache.tapestry5.commons.util.MultiKey;
    032import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
    033import org.apache.tapestry5.internal.event.InvalidationEventHubImpl;
    034import org.apache.tapestry5.internal.parser.ComponentTemplate;
    035import org.apache.tapestry5.internal.parser.TemplateToken;
    036import org.apache.tapestry5.ioc.annotations.Inject;
    037import org.apache.tapestry5.ioc.annotations.PostInjection;
    038import org.apache.tapestry5.ioc.annotations.Symbol;
    039import org.apache.tapestry5.ioc.internal.util.URLChangeTracker;
    040import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
    041import org.apache.tapestry5.ioc.services.ThreadLocale;
    042import org.apache.tapestry5.ioc.services.UpdateListener;
    043import org.apache.tapestry5.ioc.services.UpdateListenerHub;
    044import org.apache.tapestry5.model.ComponentModel;
    045import org.apache.tapestry5.services.pageload.ComponentRequestSelectorAnalyzer;
    046import org.apache.tapestry5.services.pageload.ComponentResourceLocator;
    047import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
    048import org.apache.tapestry5.services.templates.ComponentTemplateLocator;
    049import org.slf4j.Logger;
    050
    051/**
    052 * Service implementation that manages a cache of parsed component templates.
    053 */
    054public final class ComponentTemplateSourceImpl extends InvalidationEventHubImpl implements ComponentTemplateSource,
    055        UpdateListener
    056{
    057    private final TemplateParser parser;
    058
    059    private final URLChangeTracker<TemplateTrackingInfo> tracker;
    060
    061    private final ComponentResourceLocator locator;
    062
    063    private final ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer;
    064
    065    private final ThreadLocale threadLocale;
    066
    067    private final Logger logger;
    068
    069    private final boolean multipleClassLoaders;
    070
    071    /**
    072     * Caches from a key (combining component name and locale) to a resource. Often, many different keys will point to
    073     * the same resource (i.e., "foo:en_US", "foo:en_UK", and "foo:en" may all be parsed from the same "foo.tml"
    074     * resource). The resource may end up being null, meaning the template does not exist in any locale.
    075     */
    076    private final Map<MultiKey, Resource> templateResources = CollectionFactory.newConcurrentMap();
    077
    078    /**
    079     * Cache of parsed templates, keyed on resource.
    080     */
    081    private final Map<Resource, ComponentTemplate> templates = CollectionFactory.newConcurrentMap();
    082
    083    private final ComponentTemplate missingTemplate = new ComponentTemplate()
    084    {
    085        public Map<String, Location> getComponentIds()
    086        {
    087            return Collections.emptyMap();
    088        }
    089
    090        public Resource getResource()
    091        {
    092            return null;
    093        }
    094
    095        public List<TemplateToken> getTokens()
    096        {
    097            return Collections.emptyList();
    098        }
    099
    100        public boolean isMissing()
    101        {
    102            return true;
    103        }
    104
    105        public List<TemplateToken> getExtensionPointTokens(String extensionPointId)
    106        {
    107            return null;
    108        }
    109
    110        public boolean isExtension()
    111        {
    112            return false;
    113        }
    114
    115        public boolean usesStrictMixinParameters()
    116        {
    117            return false;
    118        }
    119    };
    120
    121    public ComponentTemplateSourceImpl(@Inject
    122                                       @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
    123                                       boolean productionMode,
    124                                       @Inject
    125                                       @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS)
    126                                       boolean multipleClassLoaders,
    127                                       TemplateParser parser, ComponentResourceLocator locator,
    128                                       ClasspathURLConverter classpathURLConverter,
    129                                       ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer,
    130                                       ThreadLocale threadLocale, Logger logger)
    131    {
    132        this(productionMode, multipleClassLoaders, parser, locator, new URLChangeTracker<TemplateTrackingInfo>(classpathURLConverter), componentRequestSelectorAnalyzer, threadLocale, logger);
    133    }
    134
    135    ComponentTemplateSourceImpl(boolean productionMode, boolean multipleClassLoaders, TemplateParser parser, ComponentResourceLocator locator,
    136                                URLChangeTracker<TemplateTrackingInfo> tracker, ComponentRequestSelectorAnalyzer componentRequestSelectorAnalyzer,
    137                                ThreadLocale threadLocale, Logger logger)
    138    {
    139        super(productionMode, logger);
    140
    141        this.parser = parser;
    142        this.locator = locator;
    143        this.tracker = tracker;
    144        this.componentRequestSelectorAnalyzer = componentRequestSelectorAnalyzer;
    145        this.threadLocale = threadLocale;
    146        this.logger = logger;
    147        this.multipleClassLoaders = multipleClassLoaders;
    148    }
    149
    150    @PostInjection
    151    public void registerAsUpdateListener(UpdateListenerHub hub)
    152    {
    153        hub.addUpdateListener(this);
    154    }
    155
    156    @PostInjection
    157    public void setupReload(ReloadHelper helper)
    158    {
    159        helper.addReloadCallback(new Runnable()
    160        {
    161            public void run()
    162            {
    163                invalidate();
    164            }
    165        });
    166    }
    167
    168    public ComponentTemplate getTemplate(ComponentModel componentModel, ComponentResourceSelector selector)
    169    {
    170        String componentName = componentModel.getComponentClassName();
    171
    172        MultiKey key = new MultiKey(componentName, selector);
    173
    174        // First cache is key to resource.
    175
    176        Resource resource = templateResources.get(key);
    177
    178        if (resource == null)
    179        {
    180            resource = locateTemplateResource(componentModel, selector);
    181            templateResources.put(key, resource);
    182        }
    183
    184        // If we haven't yet parsed the template into the cache, do so now.
    185
    186        ComponentTemplate result = templates.get(resource);
    187
    188        if (result == null)
    189        {
    190            result = parseTemplate(resource, componentModel.getComponentClassName());
    191            templates.put(resource, result);
    192        }
    193
    194        return result;
    195    }
    196
    197    /**
    198     * Resolves the component name to a localized {@link Resource} (using the {@link ComponentTemplateLocator} chain of
    199     * command service). The localized resource is used as the key to a cache of {@link ComponentTemplate}s.
    200     *
    201     * If a template doesn't exist, then the missing ComponentTemplate is returned.
    202     */
    203    public ComponentTemplate getTemplate(ComponentModel componentModel, Locale locale)
    204    {
    205        final Locale original = threadLocale.getLocale();
    206        try
    207        {
    208            threadLocale.setLocale(locale);
    209            return getTemplate(componentModel, componentRequestSelectorAnalyzer.buildSelectorForRequest());
    210        }
    211        finally {
    212            threadLocale.setLocale(original);
    213        }
    214    }
    215
    216    private ComponentTemplate parseTemplate(Resource r, String className)
    217    {
    218        // In a race condition, we may parse the same template more than once. This will likely add
    219        // the resource to the tracker multiple times. Not likely this will cause a big issue.
    220
    221        if (!r.exists())
    222            return missingTemplate;
    223
    224        tracker.add(r.toURL(), new TemplateTrackingInfo(r.getPath(), className));
    225
    226        return parser.parseTemplate(r);
    227    }
    228
    229    private Resource locateTemplateResource(ComponentModel initialModel, ComponentResourceSelector selector)
    230    {
    231        ComponentModel model = initialModel;
    232        while (model != null)
    233        {
    234            Resource localized = locator.locateTemplate(model, selector);
    235
    236            if (localized != null)
    237                return localized;
    238
    239            // Otherwise, this component doesn't have its own template ... lets work up to its
    240            // base class and check there.
    241
    242            model = model.getParentModel();
    243        }
    244
    245        // This will be a Resource whose URL is null, which will be picked up later and force the
    246        // return of the empty template.
    247
    248        return initialModel.getBaseResource().withExtension(TapestryConstants.TEMPLATE_EXTENSION);
    249    }
    250
    251    /**
    252     * Checks to see if any parsed resource has changed. If so, then all internal caches are cleared, and an
    253     * invalidation event is fired. This is brute force ... a more targeted dependency management strategy may come
    254     * later.
    255     * Actually, TAP5-2742 did exactly that! :D
    256     */
    257    public void checkForUpdates()
    258    {
    259        final Set<TemplateTrackingInfo> changedResourcesInfo = tracker.getChangedResourcesInfo();
    260        if (!changedResourcesInfo.isEmpty())
    261        {
    262            if (logger.isInfoEnabled())
    263            {
    264                logger.info("Changed template(s) found: {}", String.join(", ",
    265                        changedResourcesInfo.stream().map(TemplateTrackingInfo::getTemplate).collect(Collectors.toList())));
    266            }
    267
    268            if (multipleClassLoaders)
    269            {
    270
    271                final Iterator<Entry<MultiKey, Resource>> templateResourcesIterator = templateResources.entrySet().iterator();
    272                for (TemplateTrackingInfo info : changedResourcesInfo)
    273                {
    274                    while (templateResourcesIterator.hasNext())
    275                    {
    276                        final MultiKey key = templateResourcesIterator.next().getKey();
    277                        if (info.getClassName().equals((String) key.getValues()[0]))
    278                        {
    279                            templates.remove(templateResources.get(key));
    280                            templateResourcesIterator.remove();
    281                        }
    282                    }
    283                }
    284
    285                fireInvalidationEvent(changedResourcesInfo.stream().map(TemplateTrackingInfo::getClassName).collect(Collectors.toList()));
    286
    287            }
    288            else
    289            {
    290                invalidate();
    291            }
    292        }
    293    }
    294
    295    private void invalidate()
    296    {
    297        tracker.clear();
    298        templateResources.clear();
    299        templates.clear();
    300        fireInvalidationEvent();
    301    }
    302
    303    public InvalidationEventHub getInvalidationEventHub()
    304    {
    305        return this;
    306    }
    307}