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 }