blob: a94a0eb3b81c0da5229960e545cc8c87bbbb8cb2 [file] [log] [blame]
// Copyright 2006, 2007, 2008, 2009 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.internal.structure;
import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.*;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.internal.InternalComponentResources;
import org.apache.tapestry5.internal.services.ComponentEventImpl;
import org.apache.tapestry5.internal.services.EventImpl;
import org.apache.tapestry5.internal.services.Instantiator;
import org.apache.tapestry5.internal.util.NotificationEventCallback;
import org.apache.tapestry5.ioc.BaseLocatable;
import org.apache.tapestry5.ioc.Location;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.Defense;
import static org.apache.tapestry5.ioc.internal.util.Defense.notBlank;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.internal.util.TapestryException;
import org.apache.tapestry5.model.ComponentModel;
import org.apache.tapestry5.model.ParameterModel;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.runtime.*;
import org.slf4j.Logger;
import java.util.*;
/**
* Implements {@link RenderCommand} and represents a component within an overall page. Much of a component page
* element's behavior is delegated to user code, via a {@link org.apache.tapestry5.runtime.Component} instance.
* <p/>
* Once instantiated, a ComponentPageElement should be registered as a {@linkplain
* org.apache.tapestry5.internal.structure.Page#addLifecycleListener(org.apache.tapestry5.runtime.PageLifecycleListener)
* lifecycle listener}. This could be done inside the constructors, but that tends to complicate unit tests, so its done
* by {@link org.apache.tapestry5.internal.services.PageElementFactoryImpl}.
*/
public class ComponentPageElementImpl extends BaseLocatable implements ComponentPageElement, PageLifecycleListener
{
/**
* Placeholder for the body used when the component has no real content.
*/
private static class PlaceholderBlock implements Block, Renderable
{
public void render(MarkupWriter writer)
{
}
@Override
public String toString()
{
return "<PlaceholderBlock>";
}
}
private static final Block PLACEHOLDER_BLOCK = new PlaceholderBlock();
private static final ComponentCallback RESTORE_STATE_BEFORE_PAGE_ATTACH = new LifecycleNotificationComponentCallback()
{
public void run(Component component)
{
component.restoreStateBeforePageAttach();
}
};
private static final ComponentCallback CONTAINING_PAGE_DID_ATTACH = new LifecycleNotificationComponentCallback()
{
public void run(Component component)
{
component.containingPageDidAttach();
}
};
private static final ComponentCallback CONTAINING_PAGE_DID_DETACH = new LifecycleNotificationComponentCallback()
{
public void run(Component component)
{
component.containingPageDidDetach();
}
};
private static final ComponentCallback CONTAINING_PAGE_DID_LOAD = new LifecycleNotificationComponentCallback()
{
public void run(Component component)
{
component.containingPageDidLoad();
}
};
private static final ComponentCallback POST_RENDER_CLEANUP = new LifecycleNotificationComponentCallback()
{
public void run(Component component)
{
component.postRenderCleanup();
}
};
// For the moment, every component will have a template, even if it consists of
// just a page element to queue up a BeforeRenderBody phase.
private static void pushElements(RenderQueue queue, List<RenderCommand> list)
{
int count = size(list);
for (int i = count - 1; i >= 0; i--)
queue.push(list.get(i));
}
private static int size(List<?> list)
{
return list == null ? 0 : list.size();
}
private abstract class AbstractPhase extends AbstractComponentCallback implements RenderCommand
{
private final String name;
private MarkupWriter writer;
public AbstractPhase(String name)
{
super(sharedEvent);
this.name = name;
}
@Override
public String toString()
{
return phaseToString(name);
}
void reset(RenderQueue queue)
{
sharedEventHandler.queueCommands(queue);
sharedEventHandler.reset();
sharedEvent.reset();
writer = null;
}
void callback(boolean reverse, MarkupWriter writer)
{
this.writer = writer;
invoke(reverse, this);
}
public void run(Component component)
{
invokeComponent(component, writer, sharedEvent);
}
protected boolean getResult()
{
return sharedEventHandler.getResult();
}
protected abstract void invokeComponent(Component component, MarkupWriter writer, Event event);
}
private class SetupRenderPhase extends AbstractPhase
{
public SetupRenderPhase()
{
super("SetupRender");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.setupRender(writer, event);
}
public void render(final MarkupWriter writer, RenderQueue queue)
{
callback(false, writer);
push(queue, getResult(), beginRenderPhase, cleanupRenderPhase);
reset(queue);
}
}
private class BeginRenderPhase extends AbstractPhase
{
private BeginRenderPhase()
{
super("BeginRender");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.beginRender(writer, event);
}
public void render(final MarkupWriter writer, final RenderQueue queue)
{
callback(false, writer);
push(queue, afterRenderPhase);
push(queue, getResult(), beforeRenderTemplatePhase, null);
reset(queue);
}
}
/**
* Replaces {@link org.apache.tapestry5.internal.structure.ComponentPageElementImpl.BeginRenderPhase} when there is
* a handler for AfterRender but not BeginRender.
*/
private class OptimizedBeginRenderPhase implements RenderCommand
{
public void render(MarkupWriter writer, RenderQueue queue)
{
push(queue, afterRenderPhase);
push(queue, beforeRenderTemplatePhase);
}
@Override
public String toString()
{
return phaseToString("OptimizedBeginRenderPhase");
}
}
/**
* Reponsible for rendering the component's template. Even a component that doesn't have a template goes through
* this phase, as a synthetic template (used to trigger the rendering of the component's body) will be supplied.
*/
private class BeforeRenderTemplatePhase extends AbstractPhase
{
private BeforeRenderTemplatePhase()
{
super("BeforeRenderTemplate");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.beforeRenderTemplate(writer, event);
}
public void render(final MarkupWriter writer, final RenderQueue queue)
{
callback(false, writer);
push(queue, afterRenderTemplatePhase);
if (getResult())
pushElements(queue, template);
reset(queue);
}
}
/**
* Alternative version of BeforeRenderTemplatePhase used when the BeforeRenderTemplate render phase is not handled.
*/
private class RenderTemplatePhase implements RenderCommand
{
public void render(MarkupWriter writer, RenderQueue queue)
{
push(queue, afterRenderTemplatePhase);
pushElements(queue, template);
}
@Override
public String toString()
{
return phaseToString("RenderTemplate");
}
}
private class BeforeRenderBodyPhase extends AbstractPhase
{
private BeforeRenderBodyPhase()
{
super("BeforeRenderBody");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.beforeRenderBody(writer, event);
}
public void render(final MarkupWriter writer, RenderQueue queue)
{
callback(false, writer);
push(queue, afterRenderBodyPhase);
if (getResult() && bodyBlock != null)
queue.push(bodyBlock);
reset(queue);
}
}
private class AfterRenderBodyPhase extends AbstractPhase
{
private AfterRenderBodyPhase()
{
super("AfterRenderBody");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.afterRenderBody(writer, event);
}
public void render(final MarkupWriter writer, RenderQueue queue)
{
callback(true, writer);
push(queue, getResult(), null, beforeRenderBodyPhase);
reset(queue);
}
}
private class AfterRenderTemplatePhase extends AbstractPhase
{
private AfterRenderTemplatePhase()
{
super("AfterRenderTemplate");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.afterRenderTemplate(writer, event);
}
public void render(final MarkupWriter writer, final RenderQueue queue)
{
callback(true, writer);
push(queue, getResult(), null, beforeRenderTemplatePhase);
reset(queue);
}
}
private class AfterRenderPhase extends AbstractPhase
{
private AfterRenderPhase()
{
super("AfterRender");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.afterRender(writer, event);
}
public void render(final MarkupWriter writer, RenderQueue queue)
{
callback(true, writer);
push(queue, getResult(), cleanupRenderPhase, beginRenderPhase);
reset(queue);
}
}
private final ComponentPageElementResources elementResources;
private class CleanupRenderPhase extends AbstractPhase
{
private CleanupRenderPhase()
{
super("CleanupRender");
}
protected void invokeComponent(Component component, MarkupWriter writer, Event event)
{
component.cleanupRender(writer, event);
}
public void render(final MarkupWriter writer, RenderQueue queue)
{
callback(true, writer);
push(queue, getResult(), null, setupRenderPhase);
reset(queue);
}
}
private class PostRenderCleanupPhase implements RenderCommand
{
/**
* Used to detect mismatches calls to {@link MarkupWriter#element(String, Object[])} } and {@link
* org.apache.tapestry5.MarkupWriter#end()}. The expectation is that any element(s) begun by this component
* during rendering will be balanced by end() calls, resulting in the current element reverting to its initial
* value.
*/
private final Element expectedElementAtCompletion;
PostRenderCleanupPhase(Element expectedElementAtCompletion)
{
this.expectedElementAtCompletion = expectedElementAtCompletion;
}
public void render(MarkupWriter writer, RenderQueue queue)
{
rendering = false;
Element current = writer.getElement();
if (current != expectedElementAtCompletion)
throw new TapestryException(StructureMessages.unbalancedElements(completeId), getLocation(), null);
invoke(false, POST_RENDER_CLEANUP);
queue.endComponent();
// Now and only now the component is done rendering and fully cleaned up. Decrement
// the page's dirty count. If the entire render goes well, then the page will be
// clean and can be stored into the pool for later reuse.
page.decrementDirtyCount();
}
@Override
public String toString()
{
return phaseToString("PostRenderCleanup");
}
}
private Map<String, Block> blocks;
private BlockImpl bodyBlock;
private Map<String, ComponentPageElement> children;
private final String elementName;
private final Logger eventLogger;
private final String completeId;
// The user-provided class, with runtime code enhancements. In a component with mixins, this
// is the component to which the mixins are attached.
private final Component coreComponent;
/**
* Component lifecycle instances for all mixins; the core component is added to this list during page load. This is
* only used in the case that a component has mixins (in which case, the core component is listed last).
*/
private List<Component> components = null;
private final ComponentPageElement container;
private final InternalComponentResources coreResources;
private final String id;
private boolean loaded;
/**
* Map from mixin id (the simple name of the mixin class) to resources for the mixin. Created when first mixin is
* added.
*/
private Map<String, InternalComponentResources> mixinIdToComponentResources;
private final String nestedId;
private final Page page;
private boolean rendering;
// We know that, at the very least, there will be an element to force the component to render
// its body, so there's no reason to wait to initialize the list.
private final List<RenderCommand> template = CollectionFactory.newList();
private boolean renderPhasesInitalized;
private RenderCommand setupRenderPhase, beginRenderPhase, beforeRenderTemplatePhase, beforeRenderBodyPhase,
afterRenderBodyPhase, afterRenderTemplatePhase, afterRenderPhase, cleanupRenderPhase;
private final RenderPhaseEventHandler sharedEventHandler = new RenderPhaseEventHandler();
private final EventImpl sharedEvent;
/**
* Constructor for other components embedded within the root component or at deeper levels of the hierarchy.
*
* @param page ultimately containing this component
* @param container component immediately containing this component (may be null for a root component)
* @param id unique (within the container) id for this component (may be null for a root component)
* @param elementName the name of the element which represents this component in the template, or null for
* &lt;comp&gt; element or a page component
* @param instantiator used to create the new component instance and access the component's model
* @param location location of the element (within a template), used as part of exception reporting
* @param elementResources Provides access to common methods of various services
*/
ComponentPageElementImpl(Page page,
ComponentPageElement container,
String id,
String nestedId,
String completeId,
String elementName,
Instantiator instantiator,
Location location,
ComponentPageElementResources elementResources)
{
super(location);
this.page = page;
this.container = container;
this.id = id;
this.nestedId = nestedId;
this.completeId = completeId;
this.elementName = elementName;
this.elementResources = elementResources;
ComponentResources containerResources = container == null
? null
: container.getComponentResources();
coreResources = new InternalComponentResourcesImpl(this.page, this, containerResources,
this.elementResources,
completeId, nestedId, instantiator);
coreComponent = coreResources.getComponent();
Logger logger = coreResources.getLogger();
eventLogger = elementResources.getEventLogger(logger);
sharedEvent = new EventImpl(sharedEventHandler, eventLogger);
}
/**
* Constructor for the root component of a page.
*/
public ComponentPageElementImpl(Page page,
Instantiator instantiator,
ComponentPageElementResources elementResources)
{
this(page, null, null, null, page.getName(), null, instantiator, null,
elementResources);
}
private void initializeRenderPhases()
{
if (renderPhasesInitalized)
return;
setupRenderPhase = new SetupRenderPhase();
beginRenderPhase = new BeginRenderPhase();
beforeRenderTemplatePhase = new BeforeRenderTemplatePhase();
beforeRenderBodyPhase = new BeforeRenderBodyPhase();
afterRenderBodyPhase = new AfterRenderBodyPhase();
afterRenderTemplatePhase = new AfterRenderTemplatePhase();
afterRenderPhase = new AfterRenderPhase();
cleanupRenderPhase = new CleanupRenderPhase();
// Now the optimization, where we remove, replace and collapse unused phases. We use
// the component models to determine which phases have handler methods for the
// render phases.
Set<Class> handled = coreResources.getComponentModel().getHandledRenderPhases();
if (mixinIdToComponentResources != null)
{
for (ComponentResources r : mixinIdToComponentResources.values())
handled.addAll(r.getComponentModel().getHandledRenderPhases());
}
if (!handled.contains(CleanupRender.class)) cleanupRenderPhase = null;
// Now, work back to front.
if (!handled.contains(AfterRender.class))
afterRenderPhase = cleanupRenderPhase;
if (!handled.contains(AfterRenderTemplate.class))
afterRenderTemplatePhase = null;
if (!handled.contains(AfterRenderBody.class))
afterRenderBodyPhase = null;
if (!handled.contains(BeforeRenderTemplate.class))
beforeRenderTemplatePhase = new RenderTemplatePhase();
if (!handled.contains(BeginRender.class))
{
RenderCommand replacement = afterRenderPhase != null
? new OptimizedBeginRenderPhase()
: beforeRenderTemplatePhase;
beginRenderPhase = replacement;
}
if (!handled.contains(SetupRender.class))
setupRenderPhase = beginRenderPhase;
renderPhasesInitalized = true;
}
public ComponentPageElement newChild(String id, String nestedId, String completeId, String elementName,
Instantiator instantiator,
Location location)
{
ComponentPageElementImpl child = new ComponentPageElementImpl(page,
this,
id,
nestedId,
completeId,
elementName,
instantiator,
location,
elementResources);
addEmbeddedElement(child);
return child;
}
void push(RenderQueue queue, boolean forward, RenderCommand forwardPhase, RenderCommand backwardPhase)
{
push(queue, forward ? forwardPhase : backwardPhase);
}
void push(RenderQueue queue, RenderCommand nextPhase)
{
if (nextPhase != null)
queue.push(nextPhase);
}
void addEmbeddedElement(ComponentPageElement child)
{
if (children == null) children = CollectionFactory.newCaseInsensitiveMap();
String childId = child.getId();
ComponentPageElement existing = children.get(childId);
if (existing != null)
throw new TapestryException(StructureMessages.duplicateChildComponent(this, childId), child,
new TapestryException(StructureMessages.originalChildComponent(this, childId,
existing.getLocation()),
existing, null));
children.put(childId, child);
}
public void addMixin(String mixinId, Instantiator instantiator)
{
if (mixinIdToComponentResources == null)
{
mixinIdToComponentResources = CollectionFactory.newCaseInsensitiveMap();
components = CollectionFactory.newList();
}
String mixinExtension = "$" + mixinId.toLowerCase();
InternalComponentResourcesImpl resources = new InternalComponentResourcesImpl(page, this, coreResources,
elementResources,
completeId + mixinExtension,
nestedId + mixinExtension,
instantiator);
mixinIdToComponentResources.put(mixinId, resources);
components.add(resources.getComponent());
}
public void bindMixinParameter(String mixinId, String parameterName, Binding binding)
{
InternalComponentResources mixinResources = InternalUtils.get(mixinIdToComponentResources, mixinId);
mixinResources.bindParameter(parameterName, binding);
}
public Binding getBinding(String parameterName)
{
return coreResources.getBinding(parameterName);
}
public void bindParameter(String parameterName, Binding binding)
{
coreResources.bindParameter(parameterName, binding);
}
public void addToBody(RenderCommand element)
{
if (bodyBlock == null) bodyBlock = new BlockImpl(getLocation(), "Body of " + getCompleteId());
bodyBlock.addToBody(element);
}
public void addToTemplate(RenderCommand element)
{
template.add(element);
}
private void addUnboundParameterNames(String prefix, List<String> unbound, InternalComponentResources resource)
{
ComponentModel model = resource.getComponentModel();
for (String name : model.getParameterNames())
{
if (resource.isBound(name)) continue;
ParameterModel parameterModel = model.getParameterModel(name);
if (parameterModel.isRequired())
{
String fullName = prefix == null ? name : prefix + "." + name;
unbound.add(fullName);
}
}
}
public void restoreStateBeforePageAttach()
{
invoke(false, RESTORE_STATE_BEFORE_PAGE_ATTACH);
}
public void containingPageDidAttach()
{
invoke(false, CONTAINING_PAGE_DID_ATTACH);
}
public void containingPageDidDetach()
{
invoke(false, CONTAINING_PAGE_DID_DETACH);
}
public void containingPageDidLoad()
{
// If this component has mixins, add the core component to the end of the list, after the
// mixins.
if (components != null)
{
List<Component> ordered = CollectionFactory.newList();
Iterator<Component> i = components.iterator();
// Add all the normal components to the final list.
while (i.hasNext())
{
Component mixin = i.next();
if (mixin.getComponentResources().getComponentModel().isMixinAfter()) continue;
ordered.add(mixin);
// Remove from list, leaving just the late executing mixins
i.remove();
}
ordered.add(coreComponent);
// Add the remaining, late executing mixins
ordered.addAll(components);
components = ordered;
}
loaded = true;
// For some parameters, bindings (from defaults) are provided inside the callback method, so
// that is invoked first, before we check for unbound parameters.
invoke(false, CONTAINING_PAGE_DID_LOAD);
verifyRequiredParametersAreBound();
}
public void enqueueBeforeRenderBody(RenderQueue queue)
{
// TAP5-1100: In certain Ajax cases, a component may be asked to render its body
// that has never rendered itself (and thus, never called initializeRenderPhases). Subtle.
initializeRenderPhases();
// If no body, then no beforeRenderBody or afterRenderBody
if (bodyBlock != null)
push(queue, beforeRenderBodyPhase);
}
public String getCompleteId()
{
return completeId;
}
public Component getComponent()
{
return coreComponent;
}
public InternalComponentResources getComponentResources()
{
return coreResources;
}
public ComponentPageElement getContainerElement()
{
return container;
}
public Page getContainingPage()
{
return page;
}
public ComponentPageElement getEmbeddedElement(String embeddedId)
{
ComponentPageElement embeddedElement = InternalUtils.get(children, embeddedId);
if (embeddedElement == null)
{
Set<String> ids = InternalUtils.keys(children);
throw new TapestryException(StructureMessages.noSuchComponent(this, embeddedId, ids), this, null);
}
return embeddedElement;
}
public String getId()
{
return id;
}
public Logger getLogger()
{
return coreResources.getLogger();
}
public Component getMixinByClassName(String mixinClassName)
{
Component result = null;
if (mixinIdToComponentResources != null)
{
for (InternalComponentResources resources : mixinIdToComponentResources.values())
{
if (resources.getComponentModel().getComponentClassName().equals(mixinClassName))
{
result = resources.getComponent();
break;
}
}
}
if (result == null) throw new TapestryException(StructureMessages.unknownMixin(completeId, mixinClassName),
getLocation(), null);
return result;
}
public ComponentResources getMixinResources(String mixinId)
{
ComponentResources result = null;
if (mixinIdToComponentResources != null)
result = mixinIdToComponentResources.get(mixinId);
if (result == null)
throw new IllegalArgumentException(
String.format("Unable to locate mixin '%s' for component '%s'.", mixinId, completeId));
return result;
}
public String getNestedId()
{
return nestedId;
}
public boolean dispatchEvent(ComponentEvent event)
{
if (components == null)
return coreComponent.dispatchComponentEvent(event);
// Otherwise, iterate over mixins + core component
boolean result = false;
for (Component component : components)
{
result |= component.dispatchComponentEvent(event);
if (event.isAborted()) break;
}
return result;
}
/**
* Invokes a callback on the component instances (the core component plus any mixins).
*
* @param reverse if true, the callbacks are in the reverse of the normal order (this is associated with AfterXXX
* phases)
* @param callback the object to receive each component instance
*/
private void invoke(boolean reverse, ComponentCallback callback)
{
try
{ // Optimization: In the most general case (just the one component, no mixins)
// invoke the callback on the component and be done ... no iterators, no nothing.
if (components == null)
{
callback.run(coreComponent);
return;
}
Iterator<Component> i = reverse ? InternalUtils.reverseIterator(components) : components.iterator();
while (i.hasNext())
{
callback.run(i.next());
if (callback.isEventAborted()) return;
}
}
catch (RuntimeException ex)
{
throw new TapestryException(ex.getMessage(), getLocation(), ex);
}
}
public boolean isLoaded()
{
return loaded;
}
public boolean isRendering()
{
return rendering;
}
/**
* Generate a toString() for the inner classes that represent render phases.
*/
private String phaseToString(String phaseName)
{
return String.format("%s[%s]", phaseName, completeId);
}
/**
* Pushes the SetupRender phase state onto the queue.
*/
public final void render(MarkupWriter writer, RenderQueue queue)
{
// We assume that by the time we start to render, the structure (i.e., mixins) is nailed down.
// We could add a lock, but that seems wasteful.
initializeRenderPhases();
// TODO: An error if the render flag is already set (recursive rendering not
// allowed or advisable).
// Once we start rendering, the page is considered dirty, until we cleanup post render.
page.incrementDirtyCount();
// TODO: Check for recursive rendering.
rendering = true;
queue.startComponent(coreResources);
queue.push(new PostRenderCleanupPhase(writer.getElement()));
push(queue, setupRenderPhase);
}
@Override
public String toString()
{
return String.format("ComponentPageElement[%s]", completeId);
}
public boolean triggerEvent(String eventType, Object[] contextValues, ComponentEventCallback callback)
{
return triggerContextEvent(eventType,
createParameterContext(contextValues == null ? new Object[0] : contextValues),
callback);
}
private EventContext createParameterContext(final Object... values)
{
return new EventContext()
{
public int getCount()
{
return values.length;
}
public <T> T get(Class<T> desiredType, int index)
{
return elementResources.coerce(values[index], desiredType);
}
};
}
public boolean triggerContextEvent(String eventType, EventContext context, ComponentEventCallback callback)
{
Defense.notBlank(eventType, "eventType");
Defense.notNull(context, "context");
boolean result = false;
ComponentPageElement component = this;
String componentId = "";
// Provide a default handler for when the provided handler is null.
final ComponentEventCallback providedHandler = callback == null ? new NotificationEventCallback(eventType,
completeId) : callback;
ComponentEventCallback wrapped = new ComponentEventCallback()
{
public boolean handleResult(Object result)
{
// Boolean value is not passed to the handler; it will be true (abort event)
// or false (continue looking for event handlers).
if (result instanceof Boolean) return (Boolean) result;
return providedHandler.handleResult(result);
}
};
RuntimeException rootException = null;
// Because I don't like to reassign parameters.
String currentEventType = eventType;
EventContext currentContext = context;
// Track the location of the original component for the event, even as we work our way up
// the hierarchy. This may not be ideal if we trigger an "exception" event ... or maybe
// it's right (it's the location of the originally thrown exception).
Location location = component.getComponentResources().getLocation();
while (component != null)
{
try
{
Logger logger = component.getEventLogger();
ComponentEvent event = new ComponentEventImpl(currentEventType, componentId, currentContext, wrapped,
elementResources, logger);
logger.debug(TapestryMarkers.EVENT_DISPATCH, "Dispatch event: {}", event);
result |= component.dispatchEvent(event);
if (event.isAborted()) return result;
}
catch (RuntimeException ex)
{
// An exception in an event handler method
// while we're trying to handle a previous exception!
if (rootException != null) throw rootException;
// We know component is not null and therefore has a component resources that
// should have a location.
// Wrap it up to help ensure that a location is available to the event handler method or,
// more likely, to the exception report page.
rootException = new ComponentEventException(ex.getMessage(), eventType, context, location, ex);
// Switch over to triggering an "exception" event, starting in the component that
// threw the exception.
currentEventType = "exception";
currentContext = createParameterContext(rootException);
continue;
}
// On each bubble up, make the event appear to come from the previous component
// in which the event was triggered.
componentId = component.getId();
component = component.getContainerElement();
}
// If there was a handler for the exception event, it is required to return a non-null (and non-boolean) value
// to tell Tapestry what to do. Since that didn't happen, we have no choice but to rethrow the (wrapped)
// exception.
if (rootException != null) throw rootException;
return result;
}
private void verifyRequiredParametersAreBound()
{
List<String> unbound = CollectionFactory.newList();
addUnboundParameterNames(null, unbound, coreResources);
for (String name : InternalUtils.sortedKeys(mixinIdToComponentResources))
addUnboundParameterNames(name, unbound, mixinIdToComponentResources.get(name));
if (unbound.isEmpty()) return;
throw new TapestryException(StructureMessages.missingParameters(unbound, this), this, null);
}
public Locale getLocale()
{
return page.getLocale();
}
public String getElementName(String defaultElementName)
{
return elementName != null ? elementName : defaultElementName;
}
public Block getBlock(String id)
{
Block result = findBlock(id);
if (result == null)
throw new BlockNotFoundException(StructureMessages.blockNotFound(completeId, id), getLocation());
return result;
}
public Block findBlock(String id)
{
notBlank(id, "id");
return InternalUtils.get(blocks, id);
}
public void addBlock(String blockId, Block block)
{
if (blocks == null) blocks = CollectionFactory.newCaseInsensitiveMap();
if (blocks.containsKey(blockId))
throw new TapestryException(StructureMessages.duplicateBlock(this, blockId), block, null);
blocks.put(blockId, block);
}
public String getPageName()
{
return page.getName();
}
public boolean hasBody()
{
return bodyBlock != null;
}
public Block getBody()
{
return bodyBlock == null ? PLACEHOLDER_BLOCK : bodyBlock;
}
public Map<String, Binding> getInformalParameterBindings()
{
return coreResources.getInformalParameterBindings();
}
public Logger getEventLogger()
{
return eventLogger;
}
public Link createEventLink(String eventType, Object... context)
{
return elementResources.createComponentEventLink(coreResources, eventType, false, context);
}
public Link createActionLink(String eventType, boolean forForm, Object... context)
{
return elementResources.createComponentEventLink(coreResources, eventType, forForm, context);
}
public Link createFormEventLink(String eventType, Object... context)
{
return elementResources.createComponentEventLink(coreResources, eventType, true, context);
}
public Link createPageLink(String pageName, boolean override, Object... context)
{
return elementResources.createPageRenderLink(pageName, override, context);
}
public Link createPageLink(Class pageClass, boolean override, Object... context)
{
return elementResources.createPageRenderLink(pageClass, override, context);
}
}