001: // Copyright 2007 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.internal.services;
016:
017: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newCaseInsensitiveMap;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
019: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newStack;
020: import static org.apache.tapestry.ioc.internal.util.InternalUtils.isBlank;
021: import static org.apache.tapestry.ioc.internal.util.InternalUtils.isNonBlank;
022:
023: import java.util.Locale;
024: import java.util.Map;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.tapestry.Binding;
028: import org.apache.tapestry.ComponentResources;
029: import org.apache.tapestry.TapestryConstants;
030: import org.apache.tapestry.internal.bindings.LiteralBinding;
031: import org.apache.tapestry.internal.parser.AttributeToken;
032: import org.apache.tapestry.internal.parser.BlockToken;
033: import org.apache.tapestry.internal.parser.BodyToken;
034: import org.apache.tapestry.internal.parser.CommentToken;
035: import org.apache.tapestry.internal.parser.ComponentTemplate;
036: import org.apache.tapestry.internal.parser.DTDToken;
037: import org.apache.tapestry.internal.parser.EndElementToken;
038: import org.apache.tapestry.internal.parser.ExpansionToken;
039: import org.apache.tapestry.internal.parser.ParameterToken;
040: import org.apache.tapestry.internal.parser.StartComponentToken;
041: import org.apache.tapestry.internal.parser.StartElementToken;
042: import org.apache.tapestry.internal.parser.TemplateToken;
043: import org.apache.tapestry.internal.parser.TextToken;
044: import org.apache.tapestry.internal.structure.BlockImpl;
045: import org.apache.tapestry.internal.structure.BodyPageElement;
046: import org.apache.tapestry.internal.structure.ComponentPageElement;
047: import org.apache.tapestry.internal.structure.Page;
048: import org.apache.tapestry.internal.structure.PageElement;
049: import org.apache.tapestry.internal.structure.PageImpl;
050: import org.apache.tapestry.ioc.Location;
051: import org.apache.tapestry.ioc.internal.util.IdAllocator;
052: import org.apache.tapestry.ioc.internal.util.OneShotLock;
053: import org.apache.tapestry.ioc.internal.util.TapestryException;
054: import org.apache.tapestry.ioc.util.Stack;
055: import org.apache.tapestry.model.ComponentModel;
056: import org.apache.tapestry.model.EmbeddedComponentModel;
057: import org.apache.tapestry.services.BindingSource;
058: import org.apache.tapestry.services.PersistentFieldManager;
059:
060: /**
061: * Contains all the work-state related to the {@link PageLoaderImpl}.
062: */
063: class PageLoaderProcessor {
064: private static final String INHERIT_PREFIX = "inherit:";
065:
066: private static Runnable NO_OP = new Runnable() {
067: public void run() {
068: // Do nothing.
069: }
070: };
071:
072: private Stack<ComponentPageElement> _activeElementStack = newStack();
073:
074: private boolean _addAttributesAsComponentBindings = false;
075:
076: private boolean _dtdAdded;
077:
078: private final Stack<BodyPageElement> _bodyPageElementStack = newStack();
079:
080: // You can use a stack as a queue
081: private final Stack<ComponentPageElement> _componentQueue = newStack();
082:
083: private final Stack<Boolean> _discardEndTagStack = newStack();
084:
085: private final Stack<Runnable> _endElementCommandStack = newStack();
086:
087: private final IdAllocator _idAllocator = new IdAllocator();
088:
089: private final LinkFactory _linkFactory;
090:
091: private ComponentModel _loadingComponentModel;
092:
093: private ComponentPageElement _loadingElement;
094:
095: private final Map<String, Map<String, Binding>> _componentIdToBindingMap = newMap();
096:
097: private Locale _locale;
098:
099: private final OneShotLock _lock = new OneShotLock();
100:
101: private Page _page;
102:
103: private final PageElementFactory _pageElementFactory;
104:
105: private final PersistentFieldManager _persistentFieldManager;
106:
107: private final ComponentTemplateSource _templateSource;
108:
109: public PageLoaderProcessor(ComponentTemplateSource templateSource,
110: PageElementFactory pageElementFactory,
111: LinkFactory linkFactory,
112: PersistentFieldManager persistentFieldManager) {
113: _templateSource = templateSource;
114: _pageElementFactory = pageElementFactory;
115: _linkFactory = linkFactory;
116: _persistentFieldManager = persistentFieldManager;
117: }
118:
119: private void bindParameterFromTemplate(
120: ComponentPageElement component, AttributeToken token) {
121: String name = token.getName();
122: ComponentResources resources = component
123: .getComponentResources();
124:
125: // If already bound (i.e., from the component class, via @Component), then
126: // ignore the value in the template. This may need improving to just ignore
127: // the value if it is an unprefixed literal string.
128:
129: if (resources.isBound(name))
130: return;
131:
132: // Meta default of literal for the template.
133:
134: String defaultBindingPrefix = determineDefaultBindingPrefix(
135: component, name,
136: TapestryConstants.LITERAL_BINDING_PREFIX);
137:
138: Binding binding = findBinding(_loadingElement, component, name,
139: token.getValue(), defaultBindingPrefix, token
140: .getLocation());
141:
142: if (binding != null) {
143: component.bindParameter(name, binding);
144:
145: Map<String, Binding> bindingMap = _componentIdToBindingMap
146: .get(component.getCompleteId());
147: bindingMap.put(name, binding);
148: }
149: }
150:
151: private void addMixinsToComponent(ComponentPageElement component,
152: EmbeddedComponentModel model, String mixins) {
153: if (model != null) {
154: for (String mixinClassName : model.getMixinClassNames())
155: _pageElementFactory.addMixinByClassName(component,
156: mixinClassName);
157: }
158:
159: if (mixins != null) {
160: for (String type : mixins.split(","))
161: _pageElementFactory.addMixinByTypeName(component, type);
162: }
163: }
164:
165: private void bindParametersFromModel(EmbeddedComponentModel model,
166: ComponentPageElement loadingComponent,
167: ComponentPageElement component,
168: Map<String, Binding> bindingMap) {
169: for (String name : model.getParameterNames()) {
170: String value = model.getParameterValue(name);
171:
172: String defaultBindingPrefix = determineDefaultBindingPrefix(
173: component, name,
174: TapestryConstants.PROP_BINDING_PREFIX);
175:
176: Binding binding = findBinding(loadingComponent, component,
177: name, value, defaultBindingPrefix, component
178: .getLocation());
179:
180: if (binding != null) {
181: component.bindParameter(name, binding);
182:
183: // So that the binding can be shared if inherited by a subcomponent
184: bindingMap.put(name, binding);
185: }
186: }
187: }
188:
189: /**
190: * Creates a new binding, or returns an existing binding (or null) for the "inherit:" binding
191: * prefix. Mostly a wrapper around
192: * {@link BindingSource#newBinding(String, ComponentResources, ComponentResources, String, String, Location)
193: *
194: * @return the new binding, or an existing binding (if inherited), or null (if inherited, and
195: * the containing parameter is not bound)
196: */
197: private Binding findBinding(ComponentPageElement loadingComponent,
198: ComponentPageElement component, String name, String value,
199: String defaultBindingPrefix, Location location) {
200: if (value.startsWith(INHERIT_PREFIX)) {
201: String loadingParameterName = value
202: .substring(INHERIT_PREFIX.length());
203: Map<String, Binding> loadingComponentBindingMap = _componentIdToBindingMap
204: .get(loadingComponent.getCompleteId());
205:
206: // This may return null if the parameter is not bound in the loading component.
207:
208: Binding existing = loadingComponentBindingMap
209: .get(loadingParameterName);
210:
211: if (existing == null)
212: return null;
213:
214: String description = String
215: .format(
216: "InheritedBinding[parameter %s %s(inherited from %s of %s)]",
217: name, component.getCompleteId(),
218: loadingParameterName, loadingComponent
219: .getCompleteId());
220:
221: // This helps with debugging, and re-orients any thrown exceptions
222: // to the location of the inherited binding, rather than the container component's
223: // binding.
224:
225: return new InheritedBinding(description, existing, location);
226: }
227:
228: return _pageElementFactory.newBinding(name, loadingComponent
229: .getComponentResources(), component
230: .getComponentResources(), defaultBindingPrefix, value,
231: location);
232: }
233:
234: /**
235: * Determines the default binding prefix for a particular parameter.
236: *
237: * @param component
238: * the component which will have a parameter bound
239: * @param parameterName
240: * the name of the parameter
241: * @param informalParameterBindingPrefix
242: * the default to use for informal parameters
243: * @return the binding prefix
244: */
245: private String determineDefaultBindingPrefix(
246: ComponentPageElement component, String parameterName,
247: String informalParameterBindingPrefix) {
248: String defaultBindingPrefix = component
249: .getDefaultBindingPrefix(parameterName);
250:
251: return defaultBindingPrefix != null ? defaultBindingPrefix
252: : informalParameterBindingPrefix;
253: }
254:
255: private PageElement newRenderBodyElement() {
256: return _pageElementFactory
257: .newRenderBodyElement(_loadingElement);
258: }
259:
260: private void addToBody(PageElement element) {
261: _bodyPageElementStack.peek().addToBody(element);
262: }
263:
264: private void attribute(AttributeToken token) {
265: // This kind of bookkeeping is ugly, we probably should have distinct (if very similar)
266: // tokens for attributes and for parameter bindings.
267:
268: if (_addAttributesAsComponentBindings) {
269: ComponentPageElement activeElement = _activeElementStack
270: .peek();
271:
272: bindParameterFromTemplate(activeElement, token);
273: return;
274: }
275:
276: PageElement element = _pageElementFactory.newAttributeElement(
277: _loadingElement.getComponentResources(), token);
278:
279: addToBody(element);
280: }
281:
282: private void body(BodyToken token) {
283: addToBody(newRenderBodyElement());
284:
285: // BODY tokens are *not* matched by END_ELEMENT tokens. Nor will there be
286: // text or comment content "inside" the BODY.
287: }
288:
289: private void comment(CommentToken token) {
290: PageElement commentElement = _pageElementFactory
291: .newCommentElement(token);
292:
293: addToBody(commentElement);
294: }
295:
296: /**
297: * Invoked whenever a token (start, startComponent, etc.) is encountered that will eventually
298: * have a matching end token. Sets up the behavior for the end token.
299: *
300: * @param discard
301: * if true, the end is discarded (if false the end token is added to the active body
302: * element)
303: * @param command
304: * command to execute to return processor state back to what it was before the
305: * command executed
306: */
307: private void configureEnd(boolean discard, Runnable command) {
308: _discardEndTagStack.push(discard);
309: _endElementCommandStack.push(command);
310: }
311:
312: private void endElement(EndElementToken token) {
313: // discard will be false if the matching start token was for a static element, and will be
314: // true otherwise (component, block, parameter).
315:
316: boolean discard = _discardEndTagStack.pop();
317:
318: if (!discard) {
319: PageElement element = _pageElementFactory.newEndElement();
320:
321: addToBody(element);
322: }
323:
324: Runnable command = _endElementCommandStack.pop();
325:
326: // Used to return environment to prior state.
327:
328: command.run();
329: }
330:
331: private void expansion(ExpansionToken token) {
332: PageElement element = _pageElementFactory.newExpansionElement(
333: _loadingElement.getComponentResources(), token);
334:
335: addToBody(element);
336: }
337:
338: private String generateEmbeddedId(String embeddedType,
339: IdAllocator idAllocator) {
340: // Component types may be in folders; strip off the folder part for starters.
341:
342: int slashx = embeddedType.lastIndexOf("/");
343:
344: String baseId = embeddedType.substring(slashx + 1)
345: .toLowerCase();
346:
347: // The idAllocator is pre-loaded with all the component ids from the template, so even
348: // if the lower-case type matches the id of an existing component, there won't be a name
349: // collision.
350:
351: return idAllocator.allocateId(baseId);
352: }
353:
354: /**
355: * As currently implemented, this should be invoked just once and then the PageLoaderProcessor
356: * instance should be discarded.
357: */
358: public Page loadPage(String logicalPageName, String pageClassName,
359: Locale locale) {
360: // Ensure that loadPage() may only be invoked once.
361:
362: _lock.lock();
363:
364: _locale = locale;
365:
366: _page = new PageImpl(logicalPageName, _locale, _linkFactory,
367: _persistentFieldManager);
368:
369: loadRootComponent(pageClassName);
370:
371: workComponentQueue();
372:
373: // The page is *loaded* before it is attached to the request.
374: // This is to help ensure that no client-specific information leaks
375: // into the page.
376:
377: _page.loaded();
378:
379: return _page;
380: }
381:
382: private void loadRootComponent(String className) {
383: ComponentPageElement rootComponent = _pageElementFactory
384: .newRootComponentElement(_page, className);
385:
386: _page.setRootElement(rootComponent);
387:
388: _componentQueue.push(rootComponent);
389: }
390:
391: /**
392: * Do you smell something? I'm smelling that this class needs to be redesigned to not need a
393: * central method this large and hard to test. I think a lot of instance and local variables
394: * need to be bundled up into some kind of process object. This code is effectively too big to
395: * be tested except through integration testing.
396: */
397: private void loadTemplateForComponent(
398: ComponentPageElement loadingElement) {
399: _loadingElement = loadingElement;
400: _loadingComponentModel = loadingElement.getComponentResources()
401: .getComponentModel();
402:
403: String componentClassName = _loadingComponentModel
404: .getComponentClassName();
405: ComponentTemplate template = _templateSource.getTemplate(
406: _loadingComponentModel, _locale);
407:
408: // When the template for a component is missing, we pretend it consists of just a RenderBody
409: // phase. Missing is not an error ... many component simply do not have a template.
410:
411: if (template.isMissing()) {
412: _loadingElement.addToTemplate(newRenderBodyElement());
413: return;
414: }
415:
416: // Pre-allocate ids to avoid later name collisions.
417:
418: Log log = _loadingComponentModel.getLog();
419:
420: // Don't have a case-insensitive Set, so we'll make due with a Map
421: Map<String, Boolean> embeddedIds = newCaseInsensitiveMap();
422:
423: for (String id : _loadingComponentModel
424: .getEmbeddedComponentIds())
425: embeddedIds.put(id, true);
426:
427: _idAllocator.clear();
428:
429: for (String id : template.getComponentIds()) {
430: _idAllocator.allocateId(id);
431: embeddedIds.remove(id);
432: }
433:
434: if (!embeddedIds.isEmpty())
435: log.error(ServicesMessages.embeddedComponentsNotInTemplate(
436: embeddedIds.keySet(), componentClassName));
437:
438: _addAttributesAsComponentBindings = false;
439:
440: // The outermost elements of the template belong in the loading component's template list,
441: // not its body list. This shunt allows everyone else to not have to make that decision,
442: // they can add to the "body" and (if there isn't an active component), the shunt will
443: // add the element to the component's template.
444:
445: BodyPageElement shunt = new BodyPageElement() {
446: public void addToBody(PageElement element) {
447: _loadingElement.addToTemplate(element);
448: }
449: };
450:
451: _bodyPageElementStack.push(shunt);
452:
453: for (TemplateToken token : template.getTokens()) {
454: switch (token.getTokenType()) {
455: case TEXT:
456: text((TextToken) token);
457: break;
458:
459: case EXPANSION:
460: expansion((ExpansionToken) token);
461: break;
462:
463: case BODY:
464: body((BodyToken) token);
465: break;
466:
467: case START_ELEMENT:
468: startElement((StartElementToken) token);
469: break;
470:
471: case START_COMPONENT:
472: startComponent((StartComponentToken) token);
473: break;
474:
475: case ATTRIBUTE:
476: attribute((AttributeToken) token);
477: break;
478:
479: case END_ELEMENT:
480: endElement((EndElementToken) token);
481: break;
482:
483: case COMMENT:
484: comment((CommentToken) token);
485: break;
486:
487: case BLOCK:
488: block((BlockToken) token);
489: break;
490:
491: case PARAMETER:
492: parameter((ParameterToken) token);
493: break;
494:
495: case DTD:
496: dtd((DTDToken) token);
497: break;
498:
499: default:
500: throw new IllegalStateException("Not implemented yet: "
501: + token);
502: }
503: }
504:
505: // For neatness / symmetry:
506:
507: _bodyPageElementStack.pop(); // the shunt
508:
509: // TODO: Check that all stacks are empty. That should never happen, as long
510: // as the ComponentTemplate is valid.
511: }
512:
513: private void parameter(ParameterToken token) {
514: BlockImpl block = new BlockImpl(token.getLocation());
515: String name = token.getName();
516:
517: Binding binding = new LiteralBinding("block parameter " + name,
518: block, token.getLocation());
519:
520: // TODO: Check that the t:parameter doesn't appear outside of an embedded component.
521:
522: _activeElementStack.peek().bindParameter(name, binding);
523:
524: setupBlock(block);
525: }
526:
527: private void setupBlock(BodyPageElement block) {
528: _bodyPageElementStack.push(block);
529:
530: Runnable cleanup = new Runnable() {
531: public void run() {
532: _bodyPageElementStack.pop();
533: }
534: };
535:
536: configureEnd(true, cleanup);
537: }
538:
539: private void block(BlockToken token) {
540: // Don't use the page element factory here becauses we need something that is both Block and
541: // BodyPageElement and don't want to use casts.
542:
543: BlockImpl block = new BlockImpl(token.getLocation());
544:
545: String id = token.getId();
546:
547: if (id != null)
548: _loadingElement.addBlock(id, block);
549:
550: setupBlock(block);
551: }
552:
553: private void startComponent(StartComponentToken token) {
554: String elementName = token.getElementName();
555:
556: // Initial guess: the type from the token (but this may be null in many cases).
557: String embeddedType = token.getComponentType();
558:
559: String embeddedId = token.getId();
560:
561: String embeddedComponentClassName = null;
562:
563: // We know that if embeddedId is null, embeddedType is not.
564:
565: if (embeddedId == null)
566: embeddedId = generateEmbeddedId(embeddedType, _idAllocator);
567:
568: EmbeddedComponentModel embeddedModel = _loadingComponentModel
569: .getEmbeddedComponentModel(embeddedId);
570:
571: if (embeddedModel != null) {
572: String modelType = embeddedModel.getComponentType();
573:
574: if (isNonBlank(modelType) && embeddedType != null) {
575: Log log = _loadingComponentModel.getLog();
576: log.error(ServicesMessages.compTypeConflict(embeddedId,
577: embeddedType, modelType));
578: }
579:
580: embeddedType = modelType;
581: embeddedComponentClassName = embeddedModel
582: .getComponentClassName();
583: }
584:
585: if (isBlank(embeddedType)
586: && isBlank(embeddedComponentClassName))
587: throw new TapestryException(ServicesMessages
588: .noTypeForEmbeddedComponent(embeddedId,
589: _loadingComponentModel
590: .getComponentClassName()), token,
591: null);
592:
593: ComponentPageElement newComponent = _pageElementFactory
594: .newComponentElement(_page, _loadingElement,
595: embeddedId, embeddedType,
596: embeddedComponentClassName, elementName, token
597: .getLocation());
598:
599: addMixinsToComponent(newComponent, embeddedModel, token
600: .getMixins());
601:
602: Map<String, Binding> bindingMap = newMap();
603: _componentIdToBindingMap.put(newComponent.getCompleteId(),
604: bindingMap);
605:
606: if (embeddedModel != null)
607: bindParametersFromModel(embeddedModel, _loadingElement,
608: newComponent, bindingMap);
609:
610: addToBody(newComponent);
611:
612: // Remember to load the template for this new component
613: _componentQueue.push(newComponent);
614:
615: // Any attribute tokens that immediately follow should be
616: // used to bind parameters.
617:
618: _addAttributesAsComponentBindings = true;
619:
620: // Any attributes (including component parameters) that come up belong on this component.
621:
622: _activeElementStack.push(newComponent);
623:
624: // Set things up so that content inside the component is added to the component's body.
625:
626: _bodyPageElementStack.push(newComponent);
627:
628: // And clean that up when the end element is reached.
629:
630: Runnable cleanup = new Runnable() {
631: public void run() {
632: _activeElementStack.pop();
633: _bodyPageElementStack.pop();
634: }
635: };
636:
637: // The start tag is not added to the body of the component, so neither should
638: // the end tag.
639: configureEnd(true, cleanup);
640: }
641:
642: private void startElement(StartElementToken token) {
643: PageElement element = _pageElementFactory
644: .newStartElement(token);
645:
646: addToBody(element);
647:
648: // Controls how attributes are interpretted.
649: _addAttributesAsComponentBindings = false;
650:
651: // Start will be matched by end:
652:
653: // Do NOT discard the end tag; add it to the body.
654:
655: configureEnd(false, NO_OP);
656: }
657:
658: private void text(TextToken token) {
659: PageElement element = _pageElementFactory.newTextElement(token);
660:
661: addToBody(element);
662: }
663:
664: private void dtd(DTDToken token) {
665: // first DTD encountered wins.
666: if (_dtdAdded)
667: return;
668:
669: PageElement element = _pageElementFactory.newDTDElement(token);
670: // since rendering via the markup writer is to the document tree,
671: // we don't really care where this gets placed in the tree; the
672: // DTDPageElement will set the dtd of the document directly, rather than
673: // writing anything to the markup writer
674: _page.getRootElement().addToTemplate(element);
675:
676: _dtdAdded = true;
677: }
678:
679: /** Works the component queue, until exausted. */
680: private void workComponentQueue() {
681: while (!_componentQueue.isEmpty()) {
682: ComponentPageElement componentElement = _componentQueue
683: .pop();
684:
685: loadTemplateForComponent(componentElement);
686: }
687: }
688: }
|