/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you 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.click; | |
import java.util.ArrayList; | |
import java.util.LinkedHashSet; | |
import java.util.List; | |
import java.util.Set; | |
import org.apache.click.service.ConfigService; | |
import org.apache.click.service.LogService; | |
import org.apache.commons.lang.Validate; | |
/** | |
* Provides a centralized registry where Controls can be registered and interact | |
* with the Click runtime. | |
* <p/> | |
* The primary use of the ControlRegistry is for Controls to register themselves | |
* as potential <tt>targets</tt> of Ajax requests | |
* (If a control is an Ajax request target, it's <tt>onProcess()</tt> | |
* method is invoked while other controls are not processed). | |
* <p/> | |
* Registering controls as Ajax targets serves a dual purpose. In addition to | |
* being potential Ajax targets, these controls will have all their Behaviors | |
* processed by the Click runtime. | |
* <p/> | |
* Thus the ControlRegistry provides the Click runtime with easy access to Controls | |
* that want to be processed for Ajax requests. It also provides quick access | |
* to Controls that have Behaviors, and particularly AjaxBehaviors that want to | |
* handle and respond to Ajax requests. | |
* | |
* <h3>Register Control as an Ajax Target</h3> | |
* Below is an example of a Control registering itself as an Ajax target: | |
* | |
* <pre class="prettyprint"> | |
* public class AbstractControl implements Control { | |
* | |
* public void addBehavior(Behavior behavior) { | |
* getBehaviors().add(behavior); | |
* // Adding a behavior also registers the Control as an Ajax target | |
* ControlRegistry.registerAjaxTarget(this); | |
* } | |
* } </pre> | |
* | |
* <h3>Register Interceptor</h3> | |
* Below is an example of a Container registering a Behavior in order to intercept | |
* and decorate its child controls: | |
* | |
* <pre class="prettyprint"> | |
* public class MyContainer extends AbstractContainer { | |
* | |
* public void onInit() { | |
* Behavior controlInterceptor = getInterceptor(); | |
* ControlRegistry.registerInterceptor(this, controlInterceptor); | |
* } | |
* | |
* private Behavior getInterceptor() { | |
* Behavior controlInterceptor = new Behavior() { | |
* | |
* // This method is invoked before the controls are rendered to the client | |
* public void preResponse(Control source) { | |
* // Here we can add a CSS class attribute to each child control | |
* addCssClassToChildControls(); | |
* } | |
* | |
* // This method is invoked before the HEAD elements are retrieved for each Control | |
* public void preRenderHeadElements(Control source) { | |
* } | |
* | |
* // This method is invoked before the Control onDestroy event | |
* public void preDestroy(Control source) { | |
* } | |
* }; | |
* return controlInterceptor; | |
* } | |
* } </pre> | |
*/ | |
public class ControlRegistry { | |
// Constants -------------------------------------------------------------- | |
/** The thread local registry holder. */ | |
private static final ThreadLocal<RegistryStack> THREAD_LOCAL_REGISTRY_STACK = | |
new ThreadLocal<RegistryStack>(); | |
// Variables -------------------------------------------------------------- | |
/** The set of Ajax target controls. */ | |
Set<Control> ajaxTargetControls; | |
/** The list of registered interceptors. */ | |
List<InterceptorHolder> interceptors; | |
/** The application log service. */ | |
LogService logger; | |
// Constructors ----------------------------------------------------------- | |
/** | |
* Construct the ControlRegistry with the given ConfigService. | |
* | |
* @param configService the click application configuration service | |
*/ | |
public ControlRegistry(ConfigService configService) { | |
this.logger = configService.getLogService(); | |
} | |
// Public Methods --------------------------------------------------------- | |
/** | |
* Return the thread local ControlRegistry instance. | |
* | |
* @return the thread local ControlRegistry instance. | |
* @throws RuntimeException if a ControlRegistry is not available on the | |
* thread | |
*/ | |
public static ControlRegistry getThreadLocalRegistry() { | |
return getRegistryStack().peek(); | |
} | |
/** | |
* Returns true if a ControlRegistry instance is available on the current | |
* thread, false otherwise. | |
* <p/> | |
* Unlike {@link #getThreadLocalRegistry()} this method can safely be used | |
* and will not throw an exception if a ControlRegistry is not available on | |
* the current thread. | |
* | |
* @return true if an ControlRegistry instance is available on the | |
* current thread, false otherwise | |
*/ | |
public static boolean hasThreadLocalRegistry() { | |
RegistryStack registryStack = THREAD_LOCAL_REGISTRY_STACK.get(); | |
if (registryStack == null) { | |
return false; | |
} | |
return !registryStack.isEmpty(); | |
} | |
/** | |
* Register the control to be processed by the Click runtime if the control | |
* is the Ajax target. A control is an Ajax target if the | |
* {@link Control#isAjaxTarget(org.apache.click.Context)} method returns true. | |
* Once a target control is identified, Click invokes its | |
* {@link Control#onProcess()} method. | |
* <p/> | |
* This method serves a dual purpose as all controls registered here | |
* will also have their Behaviors (if any) processed. Processing | |
* {@link org.apache.click.Behavior Behaviors} | |
* means their interceptor methods will be invoked during the request | |
* life cycle, passing the control as the argument. | |
* | |
* @param control the control to register as an Ajax target | |
*/ | |
public static void registerAjaxTarget(Control control) { | |
if (control == null) { | |
throw new IllegalArgumentException("control cannot be null"); | |
} | |
ControlRegistry instance = getThreadLocalRegistry(); | |
instance.internalRegisterAjaxTarget(control); | |
} | |
/** | |
* Register a control event interceptor for the given Control and Behavior. | |
* The control will be passed as the source control to the Behavior | |
* interceptor methods: | |
* {@link org.apache.click.Behavior#preRenderHeadElements(org.apache.click.Control) preRenderHeadElements(Control)}, | |
* {@link org.apache.click.Behavior#preResponse(org.apache.click.Control) preResponse(Control)} and | |
* {@link org.apache.click.Behavior#preDestroy(org.apache.click.Control) preDestroy(Control)}. | |
* | |
* @param control the interceptor source control | |
* @param controlInterceptor the control interceptor to register | |
*/ | |
public static void registerInterceptor(Control control, Behavior controlInterceptor) { | |
if (control == null) { | |
throw new IllegalArgumentException("control cannot be null"); | |
} | |
if (controlInterceptor == null) { | |
throw new IllegalArgumentException("control interceptor cannot be null"); | |
} | |
ControlRegistry instance = getThreadLocalRegistry(); | |
instance.internalRegisterInterceptor(control, controlInterceptor); | |
} | |
// Protected Methods ------------------------------------------------------ | |
/** | |
* Allow the registry to handle the error that occurred. | |
* | |
* @param throwable the error which occurred during processing | |
*/ | |
protected void errorOccurred(Throwable throwable) { | |
clear(); | |
} | |
// Package Private Methods ------------------------------------------------ | |
/** | |
* Remove all interceptors and ajax target controls from this registry. | |
*/ | |
void clear() { | |
if (hasInterceptors()) { | |
getInterceptors().clear(); | |
} | |
if (hasAjaxTargetControls()) { | |
getAjaxTargetControls().clear(); | |
} | |
} | |
/** | |
* Register the AJAX target control. | |
* | |
* @param control the AJAX target control | |
*/ | |
void internalRegisterAjaxTarget(Control control) { | |
Validate.notNull(control, "Null control parameter"); | |
getAjaxTargetControls().add(control); | |
} | |
/** | |
* Register the source control and associated interceptor. | |
* | |
* @param source the interceptor source control | |
* @param controlInterceptor the control interceptor to register | |
*/ | |
void internalRegisterInterceptor(Control source, Behavior controlInterceptor) { | |
Validate.notNull(source, "Null source parameter"); | |
Validate.notNull(controlInterceptor, "Null interceptor parameter"); | |
InterceptorHolder interceptorHolder = new InterceptorHolder(source, controlInterceptor); | |
// Guard against adding duplicate interceptors | |
List<InterceptorHolder> localInterceptors = getInterceptors(); | |
if (!localInterceptors.contains(interceptorHolder)) { | |
localInterceptors.add(interceptorHolder); | |
} | |
} | |
void processPreResponse(Context context) { | |
if (hasAjaxTargetControls()) { | |
for (Control control : getAjaxTargetControls()) { | |
for (Behavior behavior : control.getBehaviors()) { | |
behavior.preResponse(control); | |
} | |
} | |
} | |
if (hasInterceptors()) { | |
for (InterceptorHolder interceptorHolder : getInterceptors()) { | |
Behavior interceptor = interceptorHolder.getInterceptor(); | |
Control control = interceptorHolder.getControl(); | |
interceptor.preResponse(control); | |
} | |
} | |
} | |
void processPreRenderHeadElements(Context context) { | |
if (hasAjaxTargetControls()) { | |
for (Control control : getAjaxTargetControls()) { | |
for (Behavior behavior : control.getBehaviors()) { | |
behavior.preRenderHeadElements(control); | |
} | |
} | |
} | |
if (hasInterceptors()) { | |
for (InterceptorHolder interceptorHolder : getInterceptors()) { | |
Behavior interceptor = interceptorHolder.getInterceptor(); | |
Control control = interceptorHolder.getControl(); | |
interceptor.preRenderHeadElements(control); | |
} | |
} | |
} | |
void processPreDestroy(Context context) { | |
if (hasAjaxTargetControls()) { | |
for (Control control : getAjaxTargetControls()) { | |
for (Behavior behavior : control.getBehaviors()) { | |
behavior.preDestroy(control); | |
} | |
} | |
} | |
if (hasInterceptors()) { | |
for (InterceptorHolder interceptorHolder : getInterceptors()) { | |
Behavior interceptor = interceptorHolder.getInterceptor(); | |
Control control = interceptorHolder.getControl(); | |
interceptor.preDestroy(control); | |
} | |
} | |
} | |
/** | |
* Checks if any AJAX target control have been registered. | |
*/ | |
boolean hasAjaxTargetControls() { | |
if (ajaxTargetControls == null || ajaxTargetControls.isEmpty()) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Return the set of potential Ajax target controls. | |
* | |
* @return the set of potential Ajax target controls | |
*/ | |
Set<Control> getAjaxTargetControls() { | |
if (ajaxTargetControls == null) { | |
ajaxTargetControls = new LinkedHashSet<Control>(); | |
} | |
return ajaxTargetControls; | |
} | |
/** | |
* Checks if any control interceptors have been registered. | |
*/ | |
boolean hasInterceptors() { | |
if (interceptors == null || interceptors.isEmpty()) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Return the set of registered control interceptors. | |
* | |
* @return set of registered interceptors | |
*/ | |
List<InterceptorHolder> getInterceptors() { | |
if (interceptors == null) { | |
interceptors = new ArrayList<InterceptorHolder>(); | |
} | |
return interceptors; | |
} | |
/** | |
* Adds the specified ControlRegistry on top of the registry stack. | |
* | |
* @param controlRegistry the ControlRegistry to add | |
*/ | |
static void pushThreadLocalRegistry(ControlRegistry controlRegistry) { | |
getRegistryStack().push(controlRegistry); | |
} | |
/** | |
* Remove and return the controlRegistry instance on top of the | |
* registry stack. | |
* | |
* @return the controlRegistry instance on top of the registry stack | |
*/ | |
static ControlRegistry popThreadLocalRegistry() { | |
RegistryStack registryStack = getRegistryStack(); | |
ControlRegistry controlRegistry = registryStack.pop(); | |
if (registryStack.isEmpty()) { | |
THREAD_LOCAL_REGISTRY_STACK.set(null); | |
} | |
return controlRegistry; | |
} | |
static RegistryStack getRegistryStack() { | |
RegistryStack registryStack = THREAD_LOCAL_REGISTRY_STACK.get(); | |
if (registryStack == null) { | |
registryStack = new RegistryStack(2); | |
THREAD_LOCAL_REGISTRY_STACK.set(registryStack); | |
} | |
return registryStack; | |
} | |
/** | |
* Provides an unsynchronized Stack. | |
*/ | |
static class RegistryStack extends ArrayList<ControlRegistry> { | |
/** Serialization version indicator. */ | |
private static final long serialVersionUID = 1L; | |
/** | |
* Create a new RegistryStack with the given initial capacity. | |
* | |
* @param initialCapacity specify initial capacity of this stack | |
*/ | |
private RegistryStack(int initialCapacity) { | |
super(initialCapacity); | |
} | |
/** | |
* Pushes the ControlRegistry onto the top of this stack. | |
* | |
* @param controlRegistry the ControlRegistry to push onto this stack | |
* @return the ControlRegistry pushed on this stack | |
*/ | |
private ControlRegistry push(ControlRegistry controlRegistry) { | |
add(controlRegistry); | |
return controlRegistry; | |
} | |
/** | |
* Removes and return the ControlRegistry at the top of this stack. | |
* | |
* @return the ControlRegistry at the top of this stack | |
*/ | |
private ControlRegistry pop() { | |
ControlRegistry controlRegistry = peek(); | |
remove(size() - 1); | |
return controlRegistry; | |
} | |
/** | |
* Looks at the ControlRegistry at the top of this stack without | |
* removing it. | |
* | |
* @return the ControlRegistry at the top of this stack | |
*/ | |
private ControlRegistry peek() { | |
int length = size(); | |
if (length == 0) { | |
String msg = "No ControlRegistry available on ThreadLocal Registry Stack"; | |
throw new RuntimeException(msg); | |
} | |
return get(length - 1); | |
} | |
} | |
static class InterceptorHolder { | |
private Behavior interceptor; | |
private Control control; | |
public InterceptorHolder(Control control, Behavior interceptor) { | |
this.control = control; | |
this.interceptor = interceptor; | |
} | |
public Behavior getInterceptor() { | |
return interceptor; | |
} | |
public void setInterceptor(Behavior interceptor) { | |
this.interceptor = interceptor; | |
} | |
public Control getControl() { | |
return control; | |
} | |
public void setControl(Control control) { | |
this.control = control; | |
} | |
/** | |
* @see Object#equals(java.lang.Object) | |
* | |
* @param o the reference object with which to compare | |
* @return true if this object equals the given object | |
*/ | |
@Override | |
public boolean equals(Object o) { | |
//1. Use the == operator to check if the argument is a reference to this object. | |
if (o == this) { | |
return true; | |
} | |
//2. Use the instanceof operator to check if the argument is of the correct type. | |
if (!(o instanceof InterceptorHolder)) { | |
return false; | |
} | |
//3. Cast the argument to the correct type. | |
InterceptorHolder that = (InterceptorHolder) o; | |
boolean equals = this.control == null ? that.control == null : this.control.equals(that.control); | |
if (!equals) { | |
return false; | |
} | |
return this.interceptor == null ? that.interceptor == null : this.interceptor.equals(that.interceptor); | |
} | |
/** | |
* @see java.lang.Object#hashCode() | |
* | |
* @return the InterceptorHolder hashCode | |
*/ | |
@Override | |
public int hashCode() { | |
int result = 17; | |
result = 37 * result + (control == null ? 0 : control.hashCode()); | |
result = 37 * result + (interceptor == null ? 0 : interceptor.hashCode()); | |
return result; | |
} | |
} | |
} |