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}