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}