On 7/12/2014 3:14 PM, Garret Wilson wrote:
... The approach I am proposing allows one to encapsulate a whole
series of behaviors and component associations within one "bundle".
Another benefit of "behavior bundles" is that in Wicket 7.x, legacy
behavior (such as wrapping disabled links in <em><span>) could easily be
included in a Wicket6BehaviorBundle. This would obviate the need for
one-off backwards-compatibility classes
<https://issues.apache.org/jira/browse/WICKET-4904?focusedCommentId=14059918&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14059918>
such as DisabledLinkBehavior.LinkInstantiationListener to be included in
the main framework.
But without further ado, here is the class itself. Please tell me what
you think about it. I'd like to contribute it. (I couldn't help but use
a few Google Guava classes---they are so useful, and I've grown
accustomed to having them handy, but if it's not possible to use them I
can rip them out before submitting a patch.) Note the documentation
around multiple registration of behaviors---I didn't know what Wicket
allows in the way of a behavior added multiple times, so I took a simple
implementation and documented it.
As a reminder, the bundle is used like this (but preferably someone
would create bundle subclasses to distribute separately):
final BehaviorBundle pureCSSBehaviorBundle = new BehaviorBundle();
pureCSSBehaviorBundle.registerBehavior(AbstractLink.class, new
DisabledClassAttributeAppender("pure-button-disabled"));
getComponentInstantiationListeners().add(pureCSSBehaviorBundle);
After some discussion on the list, would it be appropriate for me to go
ahead and create a JIRA and then submit a patch? (I guess I'll be forced
to touch Git, now. ;) )
Code below. Cheers!
Garret
/**
* A collection of behaviors associated with component classes. When
added to the application's collection of component
* instantiation listeners, the designated behaviors will be registered
with the associated components and their subclasses. This
* facilities grouping behaviors based upon functionality or library.
* <p>
* For example, the <a href="http://purecss.io/">Yahoo! Pure CSS</a>
library requires that <a
* href="http://purecss.io/buttons/">disabled buttons</a> have the
<code>pure-button-disabled</code> class. A behavior bundle may
* be created supporting this feature maybe be added as follows:
* </p>
*
* <pre>
* <code>BehaviorBundle pureCSSBehaviorBundle = new BehaviorBundle();
* pureCSSBehaviorBundle.registerBehavior(AbstractLink.class, new
DisabledClassAttributeAppender("pure-button-disabled"));
* getComponentInstantiationListeners().add(pureCSSBehaviorBundle);</code>
* </pre>
* <p>
* Note that a production behavior bundle would likely create a {@link
BehaviorBundle} subclass containing the appropriate
* behaviors to be distributed as a unit.
* </p>
* @author Garret Wilson
* @see Application#getComponentInstantiationListeners()
*/
public class BehaviorBundle implements IComponentInstantiationListener {
/** The lock governing multithreaded access to the registered
component behaviors. */
final ReadWriteLock lock = new ReentrantReadWriteLock();
/** The map of behaviors, accessed under {@link #lock}. */
private final ListMultimap<Class<? extends Component>, Behavior>
behaviorMap = ArrayListMultimap.create();
/**
* Registers a behavior with a component class for adding to each
component when it is instantiated.
* <p>
* Behaviors registered with a given class will be added to instances
of any child classes as well. For example, registering a
* behavior for {@link AbstractLink} will result in the behavior
being added to instances of e.g. {@link BookmarkablePageLink}
* as well.
* </p>
* <p>
* Behaviors registered twice for one class, or at multiple places in
the hierarchy of a class, will consequently be added
* multiple times to the same component.
* </p>
* @param componentClass The class and subclasses of which the
behavior should be registered.
* @param behaviors The behavior to register for addition upon
component instantiation..
* @return The bundle itself, to allow method call chaining.
*/
public BehaviorBundle registerBehavior(final Class<? extends
Component> componentClass, final Behavior behavior) {
lock.writeLock().lock();
try {
behaviorMap.put(checkNotNull(componentClass),
checkNotNull(behavior));
} finally {
lock.writeLock().unlock();
}
return this;
}
/**
* {@inheritDoc} This implementation adds all behaviors registered
with the component's class and all parent classes up to and
* including {@link Component}. Behaviors will be added in order from
the most general (close to {@link Component} to the most
* specific (the component's actual class).
*/
@Override
public void onInstantiation(final Component component) {
lock.readLock().lock();
try {
addBehaviors(component, component.getClass());
} finally {
lock.readLock().unlock();
}
}
/**
* Adds all registered behaviors to the given component based upon
the given component class and the parent classes, stopping at
* {@link Component}.
* <p>
* Parent class behaviors are added first in order to allow general
behaviors to take effect earlier.
* </p>
* <p>
* This method should only be called under a read lock.
* </p>
* @param component The component to which the behaviors should be added.
* @param componentClass
*/
//we ensure manually that the parent classes are subclasses of Component
@SuppressWarnings("unchecked")
protected void addBehaviors(final Component component, final Class<?
extends Component> componentClass) {
//do a depth-first traversal to add general (super-class) behaviors
first
//no need to look higher up the hierarchy than Component
if (!componentClass.equals(Component.class)) {
final Class<?> parentComponentClass = componentClass.getSuperclass();
//the class is a subclass of Component; because we stop when we
find Component,
//we expect never to run out of parents
assert parentComponentClass != null;
addBehaviors(component, (Class<? extends Component>)
parentComponentClass);
}
//add behaviors (if any) registered for this class
for (final Behavior behavior : behaviorMap.get(componentClass)) {
component.add(behavior);
}
}
}