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
015package org.apache.tapestry.services.impl;
016
017import java.io.BufferedInputStream;
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.InputStreamReader;
021import java.net.URL;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.Locale;
026import java.util.Map;
027
028import org.apache.commons.logging.Log;
029import org.apache.hivemind.ApplicationRuntimeException;
030import org.apache.hivemind.Resource;
031import org.apache.tapestry.IAsset;
032import org.apache.tapestry.IComponent;
033import org.apache.tapestry.IPage;
034import org.apache.tapestry.IRequestCycle;
035import org.apache.tapestry.Tapestry;
036import org.apache.tapestry.engine.ITemplateSourceDelegate;
037import org.apache.tapestry.event.ReportStatusEvent;
038import org.apache.tapestry.event.ReportStatusListener;
039import org.apache.tapestry.event.ResetEventListener;
040import org.apache.tapestry.l10n.ResourceLocalizer;
041import org.apache.tapestry.parse.ComponentTemplate;
042import org.apache.tapestry.parse.ITemplateParser;
043import org.apache.tapestry.parse.ITemplateParserDelegate;
044import org.apache.tapestry.parse.TemplateParseException;
045import org.apache.tapestry.parse.TemplateToken;
046import org.apache.tapestry.parse.TextToken;
047import org.apache.tapestry.parse.TokenType;
048import org.apache.tapestry.resolver.ComponentSpecificationResolver;
049import org.apache.tapestry.services.ComponentPropertySource;
050import org.apache.tapestry.services.TemplateSource;
051import org.apache.tapestry.spec.IComponentSpecification;
052import 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
061public 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}