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