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