001    // Copyright 2004, 2005 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry.services.impl;
016    
017    import java.io.BufferedInputStream;
018    import java.io.IOException;
019    import java.io.InputStream;
020    import java.io.InputStreamReader;
021    import java.net.URL;
022    import java.util.Collections;
023    import java.util.HashMap;
024    import java.util.Locale;
025    import java.util.Map;
026    
027    import org.apache.commons.logging.Log;
028    import org.apache.hivemind.ApplicationRuntimeException;
029    import org.apache.hivemind.Resource;
030    import org.apache.tapestry.IAsset;
031    import org.apache.tapestry.IComponent;
032    import org.apache.tapestry.IPage;
033    import org.apache.tapestry.IRequestCycle;
034    import org.apache.tapestry.Tapestry;
035    import org.apache.tapestry.engine.ITemplateSourceDelegate;
036    import org.apache.tapestry.event.ResetEventListener;
037    import org.apache.tapestry.parse.ComponentTemplate;
038    import org.apache.tapestry.parse.ITemplateParser;
039    import org.apache.tapestry.parse.ITemplateParserDelegate;
040    import org.apache.tapestry.parse.TemplateParseException;
041    import org.apache.tapestry.parse.TemplateToken;
042    import org.apache.tapestry.resolver.ComponentSpecificationResolver;
043    import org.apache.tapestry.services.ComponentPropertySource;
044    import org.apache.tapestry.services.TemplateSource;
045    import org.apache.tapestry.spec.IComponentSpecification;
046    import org.apache.tapestry.util.MultiKey;
047    
048    /**
049     * Implementation of {@link org.apache.tapestry.services.TemplateSource}. Templates, once parsed,
050     * stay in memory until explicitly cleared.
051     * 
052     * @author Howard Lewis Ship
053     */
054    
055    public class TemplateSourceImpl implements TemplateSource, ResetEventListener
056    {
057        private Log _log;
058    
059        // The name of the component/application/etc property that will be used to
060        // determine the encoding to use when loading the template
061    
062        public static final String TEMPLATE_ENCODING_PROPERTY_NAME = "org.apache.tapestry.template-encoding";
063    
064        // Cache of previously retrieved templates. Key is a multi-key of
065        // specification resource path and locale (local may be null), value
066        // is the ComponentTemplate.
067    
068        private Map _cache = Collections.synchronizedMap(new HashMap());
069    
070        // Previously read templates; key is the Resource, value
071        // is the ComponentTemplate.
072    
073        private Map _templates = Collections.synchronizedMap(new HashMap());
074    
075        private static final int BUFFER_SIZE = 2000;
076    
077        private ITemplateParser _parser;
078    
079        /** @since 2.2 */
080    
081        private Resource _contextRoot;
082    
083        /** @since 3.0 */
084    
085        private ITemplateSourceDelegate _delegate;
086    
087        /** @since 4.0 */
088    
089        private ComponentSpecificationResolver _componentSpecificationResolver;
090    
091        /** @since 4.0 */
092    
093        private ComponentPropertySource _componentPropertySource;
094    
095        /**
096         * Clears the template cache. This is used during debugging.
097         */
098    
099        public void resetEventDidOccur()
100        {
101            _cache.clear();
102            _templates.clear();
103        }
104    
105        /**
106         * Reads the template for the component.
107         */
108    
109        public ComponentTemplate getTemplate(IRequestCycle cycle, IComponent component)
110        {
111            IComponentSpecification specification = component.getSpecification();
112            Resource resource = specification.getSpecificationLocation();
113    
114            Locale locale = component.getPage().getLocale();
115    
116            Object key = new MultiKey(new Object[]
117            { resource, locale }, false);
118    
119            ComponentTemplate result = searchCache(key);
120            if (result != null)
121                return result;
122    
123            result = findTemplate(cycle, resource, component, locale);
124    
125            if (result == null)
126            {
127                result = _delegate.findTemplate(cycle, component, locale);
128    
129                if (result != null)
130                    return result;
131    
132                String message = component.getSpecification().isPageSpecification() ? ImplMessages
133                        .noTemplateForPage(component.getExtendedId(), locale) : ImplMessages
134                        .noTemplateForComponent(component.getExtendedId(), locale);
135    
136                throw new ApplicationRuntimeException(message, component, component.getLocation(), null);
137            }
138    
139            saveToCache(key, result);
140    
141            return result;
142        }
143    
144        private ComponentTemplate searchCache(Object key)
145        {
146            return (ComponentTemplate) _cache.get(key);
147        }
148    
149        private void saveToCache(Object key, ComponentTemplate template)
150        {
151            _cache.put(key, template);
152    
153        }
154    
155        /**
156         * Finds the template for the given component, using the following rules:
157         * <ul>
158         * <li>If the component has a $template asset, use that
159         * <li>Look for a template in the same folder as the component
160         * <li>If a page in the application namespace, search in the application root
161         * <li>Fail!
162         * </ul>
163         * 
164         * @return the template, or null if not found
165         */
166    
167        private ComponentTemplate findTemplate(IRequestCycle cycle, Resource resource,
168                IComponent component, Locale locale)
169        {
170            IAsset templateAsset = component.getAsset(TEMPLATE_ASSET_NAME);
171    
172            if (templateAsset != null)
173                return readTemplateFromAsset(cycle, component, templateAsset);
174    
175            String name = resource.getName();
176            int dotx = name.lastIndexOf('.');
177            String templateExtension = getTemplateExtension(component);
178            String templateBaseName = name.substring(0, dotx + 1) + templateExtension;
179    
180            ComponentTemplate result = findStandardTemplate(
181                    cycle,
182                    resource,
183                    component,
184                    templateBaseName,
185                    locale);
186    
187            if (result == null && component.getSpecification().isPageSpecification()
188                    && component.getNamespace().isApplicationNamespace())
189                result = findPageTemplateInApplicationRoot(
190                        cycle,
191                        (IPage) component,
192                        templateExtension,
193                        locale);
194    
195            return result;
196        }
197    
198        private ComponentTemplate findPageTemplateInApplicationRoot(IRequestCycle cycle, IPage page,
199                String templateExtension, Locale locale)
200        {
201            // Note: a subtle change from release 3.0 to 4.0.
202            // In release 3.0, you could use a <page> element to define a page named Foo whose
203            // specification was Bar.page. We would then search for /Bar.page. Confusing? Yes.
204            // In 4.0, we are more reliant on the page name, which may include a folder prefix (i.e.,
205            // "admin/EditUser", so when we search it is based on the page name and not the
206            // specification resource file name. We would search for Foo.html. Moral of the
207            // story is to use the page name for the page specifiation and the template.
208    
209            String templateBaseName = page.getPageName() + "." + templateExtension;
210    
211            if (_log.isDebugEnabled())
212                _log.debug("Checking for " + templateBaseName + " in application root");
213    
214            Resource baseLocation = _contextRoot.getRelativeResource(templateBaseName);
215            Resource localizedLocation = baseLocation.getLocalization(locale);
216    
217            if (localizedLocation == null)
218                return null;
219    
220            return getOrParseTemplate(cycle, localizedLocation, page);
221        }
222    
223        /**
224         * Reads an asset to get the template.
225         */
226    
227        private ComponentTemplate readTemplateFromAsset(IRequestCycle cycle, IComponent component,
228                IAsset asset)
229        {
230            InputStream stream = asset.getResourceAsStream(cycle);
231    
232            char[] templateData = null;
233    
234            try
235            {
236                String encoding = getTemplateEncoding(component, null);
237    
238                templateData = readTemplateStream(stream, encoding);
239    
240                stream.close();
241            }
242            catch (IOException ex)
243            {
244                throw new ApplicationRuntimeException(ImplMessages.unableToReadTemplate(asset), ex);
245            }
246    
247            Resource resourceLocation = asset.getResourceLocation();
248    
249            return constructTemplateInstance(cycle, templateData, resourceLocation, component);
250        }
251    
252        /**
253         * Search for the template corresponding to the resource and the locale. This may be in the
254         * template map already, or may involve reading and parsing the template.
255         * 
256         * @return the template, or null if not found.
257         */
258    
259        private ComponentTemplate findStandardTemplate(IRequestCycle cycle, Resource resource,
260                IComponent component, String templateBaseName, Locale locale)
261        {
262            if (_log.isDebugEnabled())
263                _log.debug("Searching for localized version of template for " + resource
264                        + " in locale " + locale.getDisplayName());
265    
266            Resource baseTemplateLocation = resource.getRelativeResource(templateBaseName);
267    
268            Resource localizedTemplateLocation = baseTemplateLocation.getLocalization(locale);
269    
270            if (localizedTemplateLocation == null)
271                return null;
272    
273            return getOrParseTemplate(cycle, localizedTemplateLocation, component);
274    
275        }
276    
277        /**
278         * Returns a previously parsed template at the specified location (which must already be
279         * localized). If not already in the template Map, then the location is parsed and stored into
280         * the templates Map, then returned.
281         */
282    
283        private ComponentTemplate getOrParseTemplate(IRequestCycle cycle, Resource resource,
284                IComponent component)
285        {
286    
287            ComponentTemplate result = (ComponentTemplate) _templates.get(resource);
288            if (result != null)
289                return result;
290    
291            // Ok, see if it exists.
292    
293            result = parseTemplate(cycle, resource, component);
294    
295            if (result != null)
296                _templates.put(resource, result);
297    
298            return result;
299        }
300    
301        /**
302         * Reads the template for the given resource; returns null if the resource doesn't exist. Note
303         * that this method is only invoked from a synchronized block, so there shouldn't be threading
304         * issues here.
305         */
306    
307        private ComponentTemplate parseTemplate(IRequestCycle cycle, Resource resource,
308                IComponent component)
309        {
310            String encoding = getTemplateEncoding(component, resource.getLocale());
311    
312            char[] templateData = readTemplate(resource, encoding);
313            if (templateData == null)
314                return null;
315    
316            return constructTemplateInstance(cycle, templateData, resource, component);
317        }
318    
319        /**
320         * This method is currently synchronized, because {@link TemplateParser}is not threadsafe.
321         * Another good candidate for a pooling mechanism, especially because parsing a template may
322         * take a while.
323         */
324    
325        private synchronized ComponentTemplate constructTemplateInstance(IRequestCycle cycle,
326                char[] templateData, Resource resource, IComponent component)
327        {
328            String componentAttributeName = _componentPropertySource.getComponentProperty(
329                    component,
330                    "org.apache.tapestry.jwcid-attribute-name");
331    
332            ITemplateParserDelegate delegate = new DefaultParserDelegate(component,
333                    componentAttributeName, cycle, _componentSpecificationResolver);
334    
335            TemplateToken[] tokens;
336    
337            try
338            {
339                tokens = _parser.parse(templateData, delegate, resource);
340            }
341            catch (TemplateParseException ex)
342            {
343                throw new ApplicationRuntimeException(ImplMessages.unableToParseTemplate(resource), ex);
344            }
345    
346            if (_log.isDebugEnabled())
347                _log.debug("Parsed " + tokens.length + " tokens from template");
348    
349            return new ComponentTemplate(templateData, tokens);
350        }
351    
352        /**
353         * Reads the template, given the complete path to the resource. Returns null if the resource
354         * doesn't exist.
355         */
356    
357        private char[] readTemplate(Resource resource, String encoding)
358        {
359            if (_log.isDebugEnabled())
360                _log.debug("Reading template " + resource);
361    
362            URL url = resource.getResourceURL();
363    
364            if (url == null)
365            {
366                if (_log.isDebugEnabled())
367                    _log.debug("Template does not exist.");
368    
369                return null;
370            }
371    
372            if (_log.isDebugEnabled())
373                _log.debug("Reading template from URL " + url);
374    
375            InputStream stream = null;
376    
377            try
378            {
379                stream = url.openStream();
380    
381                return readTemplateStream(stream, encoding);
382            }
383            catch (IOException ex)
384            {
385                throw new ApplicationRuntimeException(ImplMessages.unableToReadTemplate(resource), ex);
386            }
387            finally
388            {
389                Tapestry.close(stream);
390            }
391    
392        }
393    
394        /**
395         * Reads a Stream into memory as an array of characters.
396         */
397    
398        private char[] readTemplateStream(InputStream stream, String encoding) throws IOException
399        {
400            char[] charBuffer = new char[BUFFER_SIZE];
401            StringBuffer buffer = new StringBuffer();
402    
403            InputStreamReader reader;
404            if (encoding != null)
405                reader = new InputStreamReader(new BufferedInputStream(stream), encoding);
406            else
407                reader = new InputStreamReader(new BufferedInputStream(stream));
408    
409            try
410            {
411                while (true)
412                {
413                    int charsRead = reader.read(charBuffer, 0, BUFFER_SIZE);
414    
415                    if (charsRead <= 0)
416                        break;
417    
418                    buffer.append(charBuffer, 0, charsRead);
419                }
420            }
421            finally
422            {
423                reader.close();
424            }
425    
426            // OK, now reuse the charBuffer variable to
427            // produce the final result.
428    
429            int length = buffer.length();
430    
431            charBuffer = new char[length];
432    
433            // Copy the character out of the StringBuffer and into the
434            // array.
435    
436            buffer.getChars(0, length, charBuffer, 0);
437    
438            return charBuffer;
439        }
440    
441        /**
442         * Checks for the {@link Tapestry#TEMPLATE_EXTENSION_PROPERTY}in the component's specification,
443         * then in the component's namespace's specification. Returns
444         * {@link Tapestry#DEFAULT_TEMPLATE_EXTENSION}if not otherwise overriden.
445         */
446    
447        private String getTemplateExtension(IComponent component)
448        {
449            return _componentPropertySource.getComponentProperty(
450                    component,
451                    Tapestry.TEMPLATE_EXTENSION_PROPERTY);
452        }
453    
454        private String getTemplateEncoding(IComponent component, Locale locale)
455        {
456            return _componentPropertySource.getLocalizedComponentProperty(
457                    component,
458                    locale,
459                    TEMPLATE_ENCODING_PROPERTY_NAME);
460        }
461    
462        /** @since 4.0 */
463    
464        public void setParser(ITemplateParser parser)
465        {
466            _parser = parser;
467        }
468    
469        /** @since 4.0 */
470    
471        public void setLog(Log log)
472        {
473            _log = log;
474        }
475    
476        /** @since 4.0 */
477    
478        public void setDelegate(ITemplateSourceDelegate delegate)
479        {
480            _delegate = delegate;
481        }
482    
483        /** @since 4.0 */
484    
485        public void setComponentSpecificationResolver(ComponentSpecificationResolver resolver)
486        {
487            _componentSpecificationResolver = resolver;
488        }
489    
490        /** @since 4.0 */
491        public void setContextRoot(Resource contextRoot)
492        {
493            _contextRoot = contextRoot;
494        }
495    
496        /** @since 4.0 */
497        public void setComponentPropertySource(ComponentPropertySource componentPropertySource)
498        {
499            _componentPropertySource = componentPropertySource;
500        }
501    }