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