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}