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