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 }