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