You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@click.apache.org by sa...@apache.org on 2010/06/12 11:36:43 UTC
svn commit: r953972 - in /click/trunk/click/framework/src/org/apache/click:
ActionEventDispatcher.java CallbackDispatcher.java ClickServlet.java
Author: sabob
Date: Sat Jun 12 09:36:42 2010
New Revision: 953972
URL: http://svn.apache.org/viewvc?rev=953972&view=rev
Log:
added Ajax support to ClickServlet and core classes. CLK-653
Added:
click/trunk/click/framework/src/org/apache/click/CallbackDispatcher.java
Modified:
click/trunk/click/framework/src/org/apache/click/ActionEventDispatcher.java
click/trunk/click/framework/src/org/apache/click/ClickServlet.java
Modified: click/trunk/click/framework/src/org/apache/click/ActionEventDispatcher.java
URL: http://svn.apache.org/viewvc/click/trunk/click/framework/src/org/apache/click/ActionEventDispatcher.java?rev=953972&r1=953971&r2=953972&view=diff
==============================================================================
--- click/trunk/click/framework/src/org/apache/click/ActionEventDispatcher.java (original)
+++ click/trunk/click/framework/src/org/apache/click/ActionEventDispatcher.java Sat Jun 12 09:36:42 2010
@@ -19,36 +19,28 @@
package org.apache.click;
import java.util.ArrayList;
+import java.util.Iterator;
+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.click.util.HtmlStringBuffer;
+import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.Validate;
/**
- * Provides a control ActionListener event dispatcher.
- * <p/>
- * <b>Please note:</b> this class is meant for component development and can be
- * ignored otherwise.
- * <p/>
- * When registering an ActionListener you can specify the callback to occur for
- * a specific event. For example ActionListeners can be registered to fire
- * <tt>after</tt> the <tt>onProcess</tt> event. This event can be specified
- * through the constant {@link #POST_ON_PROCESS_EVENT}.
- * <p/>
- * The ClickServlet will notify the ActionEventDispatcher which ActionListeners
- * to fire. For example, after the <tt>onProcess</tt> event, the ClickServlet
- * will notify the dispatcher to fire ActionListeners registered for the
- * {@link #POST_ON_PROCESS_EVENT} (this is the default event when listeners are
- * fired).
- * <p/>
- * Out of the box ActionEventDispatcher only supports the event
- * {@link #POST_ON_PROCESS_EVENT} (the default).
+ * Provides a control ActionListener and Behavior dispatcher. The
+ * ClickServlet will dispatch registered ActionListeners and Behaviors after
+ * page controls have been processed.
*
* <h4>Example Usage</h4>
* The following example shows how to register an ActionListener with a custom
* Control:
*
* <pre class="prettyprint">
- * public class MyLink extends AbstractControl {
+ * public class MyControl extends AbstractControl {
* ...
*
* public boolean onProcess() {
@@ -64,10 +56,10 @@ import org.apache.commons.lang.Validate;
* }
* } </pre>
*
- * In this example if the link is clicked, it then calls
+ * When the link is clicked it invokes the method
* {@link org.apache.click.control.AbstractControl#dispatchActionEvent()}.
- * This method registers the Control's action listener with ActionEventDispatcher.
- * The ClickServlet will subsequently invoke the registered
+ * This method registers the Control's action listener with the
+ * ActionEventDispatcher. The ClickServlet will subsequently invoke the registered
* {@link ActionListener#onAction(Control)} method after all the Page controls
* <tt>onProcess()</tt> method have been invoked.
*/
@@ -75,77 +67,114 @@ public class ActionEventDispatcher {
// Constants --------------------------------------------------------------
+ /** The thread local dispatcher holder. */
+ private static final ThreadLocal<DispatcherStack> THREAD_LOCAL_DISPATCHER
+ = new ThreadLocal<DispatcherStack>();
+
+ // Variables --------------------------------------------------------------
+
+ /** The list of registered event sources. */
+ List<Control> eventSourceList;
+
+ /** The list of registered event listeners. */
+ List<ActionListener> eventListenerList;
+
+ /** The set of Controls with attached Behaviors. */
+ Set<Control> behaviorSourceSet;
+
/**
- * Indicates the listener should fire <tt>AFTER</tt> the onProcess event.
- * The <tt>POST_ON_PROCESS_EVENT</tt> is the event during which control
- * listeners will fire.
+ * The {@link org.apache.click.Partial} response to render. This partial is
+ * set from the target Behavior.
*/
- public static final int POST_ON_PROCESS_EVENT = 300;
+ Partial partial;
- // Variables --------------------------------------------------------------
+ /** The application log service. */
+ LogService logger;
- /** The thread local dispatcher holder. */
- private static final ThreadLocal<DispatcherStack> THREAD_LOCAL_DISPATCHER
- = new ThreadLocal<DispatcherStack>();
+ // Constructors -----------------------------------------------------------
- /** The POST_PROCESS events holder. */
- private EventHolder postProcessEventHolder;
+ public ActionEventDispatcher(ConfigService configService) {
+ this.logger = configService.getLogService();
+ }
- // Public Methods ---------------------------------------------------------
+ // Public Methods ---------------------------------------------------------
/**
* Register the event source and event ActionListener to be fired by the
* ClickServlet once all the controls have been processed.
- * <p/>
- * Listeners registered by this method will be fired in the
- * {@link #POST_ON_PROCESS_EVENT}.
- *
- * @see #dispatchActionEvent(org.apache.click.Control, org.apache.click.ActionListener, int)
*
* @param source the action event source
* @param listener the event action listener
*/
public static void dispatchActionEvent(Control source, ActionListener listener) {
- dispatchActionEvent(source, listener, POST_ON_PROCESS_EVENT);
+ Validate.notNull(source, "Null source parameter");
+ Validate.notNull(listener, "Null listener parameter");
+
+ ActionEventDispatcher instance = getThreadLocalDispatcher();
+ instance.registerActionEvent(source, listener);
}
/**
- * Register the event source and event ActionListener to be fired by the
- * ClickServlet in the specified event.
+ * Register the source control which behaviors should be fired by the
+ * ClickServlet.
*
- * @param source the action event source
- * @param listener the event action listener
- * @param event the specific event to trigger the action event
+ * @param source the source control which behaviors should be fired
*/
- public static void dispatchActionEvent(Control source,
- ActionListener listener, int event) {
-
+ public static void dispatchBehavior(Control source) {
Validate.notNull(source, "Null source parameter");
- Validate.notNull(listener, "Null listener parameter");
ActionEventDispatcher instance = getThreadLocalDispatcher();
- EventHolder eventHolder = instance.getEventHolder(event);
- eventHolder.registerActionEvent(source, listener);
+ instance.registerBehaviorSource(source);
}
- // Protected Methods ------------------------------------------------------
+ /**
+ * Fire all the registered action events after the Page Controls have been
+ * processed and return true if the page should continue processing.
+ *
+ * @see #fireActionEvents(org.apache.click.Context, int)
+ *
+ * @param context the request context
+ *
+ * @return true if the page should continue processing, false otherwise
+ */
+ public boolean fireActionEvents(Context context) {
+
+ if (!hasActionEvents()) {
+ return true;
+ }
+
+ return fireActionEvents(context, getEventSourceList(), getEventListenerList());
+ }
/**
- * Allow the dispatcher to handle the error that occurred.
+ * Fire all the registered behaviors after the Page Controls have been
+ * processed and return true if the page should continue processing.
*
- * @param throwable the error which occurred during processing
+ * @see #fireBehaviors(org.apache.click.Context, java.util.Set)
+ *
+ * @param context the request context
+ *
+ * @return true if the page should continue processing, false otherwise
*/
- protected void errorOccurred(Throwable throwable) {
- // Clear the POST_ON_PROCESS_EVENT control listeners from the dispatcher
- // Registered listeners from other phases must still be invoked
- getEventHolder(ActionEventDispatcher.POST_ON_PROCESS_EVENT).clear();
+ public boolean fireBehaviors(Context context) {
+
+ if (!hasBehaviorSourceSet()) {
+ return true;
+ }
+
+ return fireBehaviors(context, getBehaviorSourceSet());
}
+ // Protected Methods ------------------------------------------------------
+
/**
- * Clear the event list.
+ * Allow the dispatcher to handle the error that occurred.
+ *
+ * @param throwable the error which occurred during processing
*/
- protected void clearEvents() {
- getPostProcessEventHolder().clear();
+ protected void errorOccurred(Throwable throwable) {
+ // Clear the control listeners and behaviors from the dispatcher
+ clear();
}
/**
@@ -153,7 +182,7 @@ public class ActionEventDispatcher {
*
* @return the thread local dispatcher instance.
* @throws RuntimeException if a ActionEventDispatcher is not available on the
- * thread.
+ * thread
*/
protected static ActionEventDispatcher getThreadLocalDispatcher() {
return getDispatcherStack().peek();
@@ -163,10 +192,6 @@ public class ActionEventDispatcher {
* Fire the actions for the given listener list and event source list which
* return true if the page should continue processing.
* <p/>
- * This method will be passed the listener list and event source list
- * of a specific event e.g. {@link #POST_ON_PROCESS_EVENT}.
- * event.
- * <p/>
* This method can be overridden if you need to customize the way events
* are fired.
*
@@ -177,20 +202,20 @@ public class ActionEventDispatcher {
*
* @return true if the page should continue processing or false otherwise
*/
- protected boolean fireActionEvents(Context context, List<Control> eventSourceList,
- List<ActionListener> eventListenerList, int event) {
+ protected boolean fireActionEvents(Context context, List eventSourceList,
+ List eventListenerList) {
boolean continueProcessing = true;
for (int i = 0, size = eventSourceList.size(); i < size; i++) {
- Control source = eventSourceList.get(0);
- ActionListener listener = eventListenerList.get(0);
+ Control source = (Control) eventSourceList.get(0);
+ ActionListener listener = (ActionListener) eventListenerList.get(0);
// Pop the first entry in the list
eventSourceList.remove(0);
eventListenerList.remove(0);
- if (!fireActionEvent(context, source, listener, event)) {
+ if (!fireActionEvent(context, source, listener)) {
continueProcessing = false;
}
}
@@ -199,92 +224,225 @@ public class ActionEventDispatcher {
}
/**
- * Fire all the registered action events after the Page Controls have been
- * processed and return true if the page should continue processing.
+ * Fire the action for the given listener and event source which
+ * return true if the page should continue processing.
* <p/>
- * @see #fireActionEvents(org.apache.click.Context, int)
+ * This method can be overridden if you need to customize the way events
+ * are fired.
*
* @param context the request context
+ * @param source the source control
+ * @param listener the listener to fire
*
- * @return true if the page should continue processing or false otherwise
+ * @return true if the page should continue processing, false otherwise
*/
- protected boolean fireActionEvents(Context context) {
- return fireActionEvents(context, ActionEventDispatcher.POST_ON_PROCESS_EVENT);
+ protected boolean fireActionEvent(Context context, Control source,
+ ActionListener listener) {
+ return listener.onAction(source);
}
/**
- * Fire all the registered action events for the specified event and return
- * true if the page should continue processing.
+ * Fire the behaviors for the given control set and return true if the page
+ * should continue processing, false otherwise.
+ * <p/>
+ * This method can be overridden if you need to customize the way behaviors
+ * are fired.
+ *
+ * @see #fireBehavior(org.apache.click.Context, org.apache.click.Control)
*
* @param context the request context
- * @param event the event which listeners to fire
+ * @param behaviorSourceSet the set of controls with attached behaviors
*
- * @return true if the page should continue processing or false otherwise
+ * @return true if the page should continue processing, false otherwise
*/
- protected boolean fireActionEvents(Context context, int event) {
- EventHolder eventHolder = getEventHolder(event);
- return eventHolder.fireActionEvents(context);
+ protected boolean fireBehaviors(Context context, Set<Control> behaviorSourceSet) {
+
+ boolean continueProcessing = true;
+
+ for (Iterator<Control> it = behaviorSourceSet.iterator(); it.hasNext(); ) {
+ Control source = it.next();
+
+ // Pop the first entry in the set
+ it.remove();
+
+ if (!fireBehavior(context, source)) {
+ continueProcessing = false;
+ }
+ }
+
+ return continueProcessing;
}
/**
- * Fire the action for the given listener and event source which
- * return true if the page should continue processing.
- * <p/>
- * This method will be passed a listener and source of a specific event
- * e.g. {@link #POST_ON_PROCESS_EVENT}.
+ * Fire the behavior for the given control and return true if the page
+ * should continue processing, false otherwise.
* <p/>
- * This method can be overridden if you need to customize the way events
+ * This method can be overridden if you need to customize the way behaviors
* are fired.
*
* @param context the request context
- * @param source the source control
- * @param listener the listener to fire
- * @param event the specific event which events to fire
+ * @param source the control which attached behaviors should be fired
*
- * @return true if the page should continue processing or false otherwise
+ * @return true if the page should continue processing, false otherwise
*/
- protected boolean fireActionEvent(Context context, Control source,
- ActionListener listener, int event) {
- return listener.onAction(source);
+ protected boolean fireBehavior(Context context, Control source) {
+
+ boolean continueProcessing = true;
+
+ for (Behavior behavior : source.getBehaviors()) {
+
+ if (behavior.isRequestTarget(context)) {
+
+ partial = behavior.onAction(source);
+
+ if (logger.isTraceEnabled()) {
+ String behaviorClassName = ClassUtils.getShortClassName(behavior.getClass());
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ buffer.append(" invoked: ");
+ buffer.append(behaviorClassName);
+ buffer.append(".isRequestTarget() : true");
+ buffer.append(" (Ajax Behavior found)");
+
+ logger.trace(buffer.toString());
+
+ if (partial == null) {
+ buffer = new HtmlStringBuffer();
+ buffer.append(" invoked: ");
+ buffer.append(behaviorClassName);
+ buffer.append(".onAction() : null");
+ buffer.append(" (*no* Partial was returned by Behavior)");
+ logger.trace(buffer.toString());
+ }
+ }
+
+ continueProcessing = false;
+ break;
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+
+ // Provide trace if no target behavior was found
+ if (continueProcessing) {
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ String sourceClassName = ClassUtils.getShortClassName(source.getClass());
+ buffer.append(" *no* target behavior found for '");
+ buffer.append(source.getName()).append("' ");
+ buffer.append(sourceClassName);
+ buffer.append(" - invoking Behavior.isRequestTarget() returned false for all behaviors");
+ logger.trace(buffer.toString());
+ }
+ }
+
+ // Ajax requests stops further processing
+ return continueProcessing;
}
+ // Package Private Methods ------------------------------------------------
+
/**
- * Return the EventHolder for the specified event.
+ * Register the event source and event ActionListener.
*
- * @param event the event which EventHolder to retrieve
+ * @param source the action event source
+ * @param listener the event action listener
+ */
+ void registerActionEvent(Control source, ActionListener listener) {
+ Validate.notNull(source, "Null source parameter");
+ Validate.notNull(listener, "Null listener parameter");
+
+ getEventSourceList().add(source);
+ getEventListenerList().add(listener);
+ }
+
+ /**
+ * Register the behavior source control.
*
- * @return the EventHolder for the specified event
+ * @param source the behavior source control
*/
- protected EventHolder getEventHolder(int event) {
- if (event == POST_ON_PROCESS_EVENT) {
- return getPostProcessEventHolder();
- } else {
- return null;
+ void registerBehaviorSource(Control source) {
+ Validate.notNull(source, "Null source parameter");
+
+ getBehaviorSourceSet().add(source);
+ }
+
+ /**
+ * Checks if any Action Events have been registered.
+ *
+ * @return true if the dispatcher has any Action Events registered
+ */
+ boolean hasActionEvents() {
+ if (eventListenerList == null || eventListenerList.isEmpty()) {
+ return false;
}
+ return true;
}
/**
- * Create a new EventHolder instance.
+ * Return the list of event listeners.
*
- * @param event the EventHolder's event
- * @return new EventHolder instance
+ * @return list of event listeners
*/
- protected EventHolder createEventHolder(int event) {
- return new EventHolder(event);
+ List<ActionListener> getEventListenerList() {
+ if (eventListenerList == null) {
+ eventListenerList = new ArrayList<ActionListener>();
+ }
+ return eventListenerList;
}
- // Package Private Methods ------------------------------------------------
+ /**
+ * Return the list of event sources.
+ *
+ * @return list of event sources
+ */
+ List<Control> getEventSourceList() {
+ if (eventSourceList == null) {
+ eventSourceList = new ArrayList<Control>();
+ }
+ return eventSourceList;
+ }
+
+ /**
+ * Clear the events and behaviors.
+ */
+ void clear() {
+ if (hasActionEvents()) {
+ getEventSourceList().clear();
+ getEventListenerList().clear();
+ getBehaviorSourceSet().clear();
+ }
+ }
+
+ /**
+ * Return the Partial Ajax response or null if no behavior was dispatched.
+ *
+ * @return the Partial Ajax response or null if no behavior was dispatched
+ */
+ Partial getPartial() {
+ return partial;
+ }
+
+ /**
+ * Return true if a control with behaviors was registered, false otherwise.
+ *
+ * @return true if a control with behaviors was registered, false otherwise.
+ */
+ boolean hasBehaviorSourceSet() {
+ if (behaviorSourceSet == null || behaviorSourceSet.isEmpty()) {
+ return false;
+ }
+ return true;
+ }
/**
- * Return the {@link #POST_ON_PROCESS_EVENT} {@link EventHolder}.
+ * Return the set of controls with attached behaviors.
*
- * @return the {@link #POST_ON_PROCESS_EVENT} {@link EventHolder}
+ * @return set of control with attached behaviors
*/
- EventHolder getPostProcessEventHolder() {
- if (postProcessEventHolder == null) {
- postProcessEventHolder = createEventHolder(POST_ON_PROCESS_EVENT);
+ Set<Control> getBehaviorSourceSet() {
+ if (behaviorSourceSet == null) {
+ behaviorSourceSet = new LinkedHashSet<Control>();
}
- return postProcessEventHolder;
+ return behaviorSourceSet;
}
/**
@@ -332,107 +490,6 @@ public class ActionEventDispatcher {
// Inner Classes ----------------------------------------------------------
/**
- * Holds the list of listeners and event sources.
- */
- public class EventHolder {
-
- /** The EventHolder's event. */
- protected int event;
-
- /** The list of registered event sources. */
- private List<Control> eventSourceList;
-
- /** The list of registered event listeners. */
- private List<ActionListener> eventListenerList;
-
- // ------------------------------------------------------- Constructors
-
- /**
- * Create a new EventHolder for the given event.
- *
- * @param event the EventHolder's event
- */
- public EventHolder(int event) {
- this.event = event;
- }
-
- /**
- * Register the event source and event ActionListener to be fired in the
- * specified event.
- *
- * @param source the action event source
- * @param listener the event action listener
- */
- public void registerActionEvent(Control source, ActionListener listener) {
- Validate.notNull(source, "Null source parameter");
- Validate.notNull(listener, "Null listener parameter");
-
- getEventSourceList().add(source);
- getEventListenerList().add(listener);
- }
-
- /**
- * Checks if any Action Events have been registered.
- *
- * @return true if the dispatcher has any Action Events registered
- */
- public boolean hasActionEvents() {
- return !(eventListenerList == null || eventListenerList.isEmpty());
- }
-
- /**
- * Return the list of event listeners.
- *
- * @return list of event listeners
- */
- public List<ActionListener> getEventListenerList() {
- if (eventListenerList == null) {
- eventListenerList = new ArrayList<ActionListener>();
- }
- return eventListenerList;
- }
-
- /**
- * Return the list of event sources.
- *
- * @return list of event sources
- */
- public List<Control> getEventSourceList() {
- if (eventSourceList == null) {
- eventSourceList = new ArrayList<Control>();
- }
- return eventSourceList;
- }
-
- /**
- * Clear the events.
- */
- public void clear() {
- if (hasActionEvents()) {
- getEventSourceList().clear();
- getEventListenerList().clear();
- }
- }
-
- /**
- * Fire all the registered action events and return true if the page should
- * continue processing.
- *
- * @param context the page request context
- * @return true if the page should continue processing or false otherwise
- */
- public boolean fireActionEvents(Context context) {
-
- if (!hasActionEvents()) {
- return true;
- }
-
- return ActionEventDispatcher.this.fireActionEvents(context,
- getEventSourceList(), getEventListenerList(), event);
- }
- }
-
- /**
* Provides an unsynchronized Stack.
*/
static class DispatcherStack extends ArrayList<ActionEventDispatcher> {
Added: click/trunk/click/framework/src/org/apache/click/CallbackDispatcher.java
URL: http://svn.apache.org/viewvc/click/trunk/click/framework/src/org/apache/click/CallbackDispatcher.java?rev=953972&view=auto
==============================================================================
--- click/trunk/click/framework/src/org/apache/click/CallbackDispatcher.java (added)
+++ click/trunk/click/framework/src/org/apache/click/CallbackDispatcher.java Sat Jun 12 09:36:42 2010
@@ -0,0 +1,336 @@
+/*
+ * 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 Control event callback dispatcher.
+ *
+ * TODO: javadoc
+ */
+public class CallbackDispatcher {
+
+ // -------------------------------------------------------------- Constants
+
+ /** The thread local dispatcher holder. */
+ private static final ThreadLocal THREAD_LOCAL_DISPATCHER = new ThreadLocal();
+
+ // -------------------------------------------------------------- Variables
+
+ /** The set of registered behavior enabled controls. */
+ Set<Control> behaviorEnabledControls;
+
+ /** The list of registered callbacks. */
+ List<CallbackHolder> callbacks;
+
+ /** The application log service. */
+ LogService logger;
+
+ // Constructors -----------------------------------------------------------
+
+ public CallbackDispatcher(ConfigService configService) {
+ this.logger = configService.getLogService();
+ }
+
+ // --------------------------------------------------------- Public Methods
+
+ public static void registerBehavior(Control control) {
+ CallbackDispatcher instance = getThreadLocalDispatcher();
+ instance.internalRegisterBehavior(control);
+ }
+
+ public static void registerCallback(Control control, Callback callback) {
+ CallbackDispatcher instance = getThreadLocalDispatcher();
+ instance.internalRegisterCallback(control, callback);
+ }
+
+ // protected methods ------------------------------------------------------
+
+ /**
+ * Allow the dispatcher to handle the error that occurred.
+ *
+ * @param throwable the error which occurred during processing
+ */
+ protected void errorOccurred(Throwable throwable) {
+ getBehaviorEnabledControls().clear();
+ getCallbacks().clear();
+ }
+
+ // ------------------------------------------------ package private methods
+
+ /**
+ * Register the behavior source control.
+ *
+ * @param source the behavior source control
+ */
+ void internalRegisterBehavior(Control source) {
+ Validate.notNull(source, "Null source parameter");
+ getBehaviorEnabledControls().add(source);
+ }
+
+ /**
+ * Register the behavior source control.
+ *
+ * @param source the behavior source control
+ */
+ void internalRegisterCallback(Control source, Callback callback) {
+ Validate.notNull(source, "Null source parameter");
+ Validate.notNull(callback, "Null callback parameter");
+
+ CallbackHolder callbackHolder = new CallbackHolder(source, callback);
+ getCallbacks().add(callbackHolder);
+ }
+
+ void processBeforeResponse(Context context) {
+ if (hasBehaviorEnabledControls()) {
+ for(Control control : getBehaviorEnabledControls()) {
+ List<Behavior> behaviors = control.getBehaviors();
+ for (Behavior behavior : behaviors) {
+ behavior.preResponse(control);
+ }
+ }
+ }
+
+ if (hasCallbacks()) {
+ for (CallbackHolder callbackHolder : getCallbacks()) {
+ Callback callback = callbackHolder.getCallback();
+ Control control = callbackHolder.getControl();
+ callback.preResponse(control);
+ }
+ }
+ }
+
+ void processBeforeGetHeadElements(Context context) {
+ if (hasBehaviorEnabledControls()) {
+ for(Control control : getBehaviorEnabledControls()) {
+ List<Behavior> behaviors = control.getBehaviors();
+ for (Behavior behavior : behaviors) {
+ behavior.preGetHeadElements(control);
+ }
+ }
+ }
+
+ if (hasCallbacks()) {
+ for (CallbackHolder callbackHolder : getCallbacks()) {
+ Callback callback = callbackHolder.getCallback();
+ Control control = callbackHolder.getControl();
+ callback.preGetHeadElements(control);
+ }
+ }
+ }
+
+ void processPreDestroy(Context context) {
+ if (hasBehaviorEnabledControls()) {
+ for(Control control : getBehaviorEnabledControls()) {
+ List<Behavior> behaviors = control.getBehaviors();
+ for (Behavior behavior : behaviors) {
+ behavior.preDestroy(control);
+ }
+ }
+ }
+
+ if (hasCallbacks()) {
+ for (CallbackHolder callbackHolder : getCallbacks()) {
+ Callback callback = callbackHolder.getCallback();
+ Control control = callbackHolder.getControl();
+ callback.preDestroy(control);
+ }
+ }
+ }
+
+ /**
+ * Checks if any control callbacks have been registered.
+ */
+ boolean hasBehaviorEnabledControls() {
+ if (behaviorEnabledControls == null || behaviorEnabledControls.isEmpty()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return the set of behavior enabled controls.
+ *
+ * @return the set of behavior enabled controls.
+ */
+ Set<Control> getBehaviorEnabledControls() {
+ if (behaviorEnabledControls == null) {
+ behaviorEnabledControls = new LinkedHashSet<Control>();
+ }
+ return behaviorEnabledControls;
+ }
+
+ /**
+ * Checks if any control callbacks have been registered.
+ */
+ boolean hasCallbacks() {
+ if (callbacks == null || callbacks.isEmpty()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return the set of registered callbacks.
+ *
+ * @return set of registered callbacks
+ */
+ List<CallbackHolder> getCallbacks() {
+ if (callbacks == null) {
+ callbacks = new ArrayList<CallbackHolder>();
+ }
+ return callbacks;
+ }
+
+ static CallbackDispatcher getThreadLocalDispatcher() {
+ return getDispatcherStack().peek();
+ }
+
+ /**
+ * Adds the specified CallbackDispatcher on top of the dispatcher stack.
+ *
+ * @param callbackDispatcher the CallbackDispatcher to add
+ */
+ static void pushThreadLocalDispatcher(CallbackDispatcher callbackDispatcher) {
+ getDispatcherStack().push(callbackDispatcher);
+ }
+
+ /**
+ * Remove and return the callbackDispatcher instance on top of the
+ * dispatcher stack.
+ *
+ * @return the callbackDispatcher instance on top of the dispatcher stack
+ */
+ static CallbackDispatcher popThreadLocalDispatcher() {
+ DispatcherStack dispatcherStack = getDispatcherStack();
+ CallbackDispatcher callbackDispatcher = dispatcherStack.pop();
+
+ if (dispatcherStack.isEmpty()) {
+ THREAD_LOCAL_DISPATCHER.set(null);
+ }
+
+ return callbackDispatcher;
+ }
+
+ static DispatcherStack getDispatcherStack() {
+ DispatcherStack dispatcherStack = (DispatcherStack) THREAD_LOCAL_DISPATCHER.get();
+
+ if (dispatcherStack == null) {
+ dispatcherStack = new DispatcherStack(2);
+ THREAD_LOCAL_DISPATCHER.set(dispatcherStack);
+ }
+
+ return dispatcherStack;
+ }
+
+ /**
+ * Provides an unsynchronized Stack.
+ */
+ static class DispatcherStack extends ArrayList {
+
+ /** Serialization version indicator. */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new DispatcherStack with the given initial capacity.
+ *
+ * @param initialCapacity specify initial capacity of this stack
+ */
+ private DispatcherStack(int initialCapacity) {
+ super(initialCapacity);
+ }
+
+ /**
+ * Pushes the CallbackDispatcher onto the top of this stack.
+ *
+ * @param callbackDispatcher the CallbackDispatcher to push onto this stack
+ * @return the CallbackDispatcher pushed on this stack
+ */
+ private CallbackDispatcher push(CallbackDispatcher callbackDispatcher) {
+ add(callbackDispatcher);
+
+ return callbackDispatcher;
+ }
+
+ /**
+ * Removes and return the CallbackDispatcher at the top of this stack.
+ *
+ * @return the CallbackDispatcher at the top of this stack
+ */
+ private CallbackDispatcher pop() {
+ CallbackDispatcher callbackDispatcher = peek();
+
+ remove(size() - 1);
+
+ return callbackDispatcher;
+ }
+
+ /**
+ * Looks at the CallbackDispatcher at the top of this stack without
+ * removing it.
+ *
+ * @return the CallbackDispatcher at the top of this stack
+ */
+ private CallbackDispatcher peek() {
+ int length = size();
+
+ if (length == 0) {
+ String msg = "No CallbackDispatcher available on ThreadLocal Dispatcher Stack";
+ throw new RuntimeException(msg);
+ }
+
+ return (CallbackDispatcher) get(length - 1);
+ }
+ }
+
+ static class CallbackHolder {
+
+ private Callback callback;
+
+ private Control control;
+
+ public CallbackHolder(Control control, Callback callback) {
+ this.control = control;
+ this.callback = callback;
+ }
+
+ public Callback getCallback() {
+ return callback;
+ }
+
+ public void setCallback(Callback callback) {
+ this.callback = callback;
+ }
+
+ public Control getControl() {
+ return control;
+ }
+
+ public void setControl(Control control) {
+ this.control = control;
+ }
+ }
+}
Modified: click/trunk/click/framework/src/org/apache/click/ClickServlet.java
URL: http://svn.apache.org/viewvc/click/trunk/click/framework/src/org/apache/click/ClickServlet.java?rev=953972&r1=953971&r2=953972&view=diff
==============================================================================
--- click/trunk/click/framework/src/org/apache/click/ClickServlet.java (original)
+++ click/trunk/click/framework/src/org/apache/click/ClickServlet.java Sat Jun 12 09:36:42 2010
@@ -20,6 +20,7 @@ package org.apache.click;
import java.io.IOException;
import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.Collections;
@@ -339,6 +340,10 @@ public class ClickServlet extends HttpSe
// Bind ActionEventDispatcher to current thread
ActionEventDispatcher.pushThreadLocalDispatcher(eventDispatcher);
+ CallbackDispatcher callbackDispatcher = createCallbackDispatcher();
+ // Bind CallbackDispatcher to current thread
+ CallbackDispatcher.pushThreadLocalDispatcher(callbackDispatcher);
+
Context context = createContext(request, response, isPost);
// Bind context to current thread
Context.pushThreadLocalContext(context);
@@ -419,6 +424,7 @@ public class ClickServlet extends HttpSe
if (request.getAttribute(MOCK_MODE_ENABLED) == null) {
Context.popThreadLocalContext();
}
+ CallbackDispatcher.popThreadLocalDispatcher();
ActionEventDispatcher.popThreadLocalDispatcher();
}
}
@@ -449,6 +455,12 @@ public class ClickServlet extends HttpSe
HttpServletResponse response, boolean isPost, Throwable exception,
Class<? extends Page> pageClass) {
+ if (isAjaxRequest(request)) {
+ handleAjaxException(request, response, isPost, exception, pageClass);
+ // Exit after handling ajax exception
+ return;
+ }
+
if (exception instanceof TemplateException) {
TemplateException te = (TemplateException) exception;
if (!te.isParseError()) {
@@ -531,6 +543,8 @@ public class ClickServlet extends HttpSe
* <p/>
* This method does not invoke the "onDestroy()" callback method.
*
+ * @see #processPageEvents(org.apache.click.Page, org.apache.click.Context)
+ *
* @param page the Page to process
* @throws Exception if an error occurs
*/
@@ -538,12 +552,31 @@ public class ClickServlet extends HttpSe
protected void processPage(Page page) throws Exception {
final Context context = page.getContext();
- final boolean isPost = context.isPost();
PageImports pageImports = createPageImports(page);
page.setPageImports(pageImports);
+ if (context.isAjaxRequest()) {
+ processAjaxPageEvents(page, context);
+ } else {
+ processPageEvents(page, context);
+ }
+ }
+
+ /**
+ * Process the given page events, invoking the "on" event callback methods
+ * and directing the response.
+ * <p/>
+ * This method does not invoke the "onDestroy()" callback method.
+ *
+ * @param page the Page which events to process
+ * @param context the request context
+ * @throws Exception if an error occurs
+ */
+ protected void processPageEvents(Page page, Context context) throws Exception {
+
ActionEventDispatcher eventDispatcher = ActionEventDispatcher.getThreadLocalDispatcher();
+ CallbackDispatcher callbackDispatcher = CallbackDispatcher.getThreadLocalDispatcher();
boolean errorOccurred = page instanceof ErrorPage;
// Support direct access of click-error.htm
@@ -551,8 +584,9 @@ public class ClickServlet extends HttpSe
ErrorPage errorPage = (ErrorPage) page;
errorPage.setMode(configService.getApplicationMode());
- // Notify the dispatcher of the error
+ // Notify the dispatcher and callbackRegistry of the error
eventDispatcher.errorOccurred(errorPage.getError());
+ callbackDispatcher.errorOccurred(errorPage.getError());
}
boolean continueProcessing = performOnSecurityCheck(page, context);
@@ -574,12 +608,14 @@ public class ClickServlet extends HttpSe
continueProcessing = performOnProcess(page, context, eventDispatcher);
if (continueProcessing) {
- performOnPostOrGet(page, context, isPost);
+ performOnPostOrGet(page, context, context.isPost());
performOnRender(page, context);
}
}
+ callbackDispatcher.processBeforeResponse(context);
+ callbackDispatcher.processBeforeGetHeadElements(context);
performRender(page, context, partial);
}
@@ -656,25 +692,31 @@ public class ClickServlet extends HttpSe
for (int i = 0, size = controls.size(); i < size; i++) {
Control control = controls.get(i);
+ int initialListenerCount = 0;
+ if (logger.isTraceEnabled()) {
+ initialListenerCount = eventDispatcher.getEventSourceList().size();
+ }
+
boolean onProcessResult = control.onProcess();
if (!onProcessResult) {
continueProcessing = false;
}
if (logger.isTraceEnabled()) {
- String controlClassName = control.getClass().getName();
- controlClassName = controlClassName.substring(
- controlClassName.lastIndexOf('.') + 1);
+ String controlClassName = ClassUtils.getShortClassName(control.getClass());
String msg = " invoked: '" + control.getName() + "' "
+ controlClassName + ".onProcess() : " + onProcessResult;
logger.trace(msg);
+
+ if (initialListenerCount != eventDispatcher.getEventSourceList().size()) {
+ logger.trace(" listener was registered while processing control");
+ }
}
}
if (continueProcessing) {
- // Fire registered action events for the POST_ON_PROCESS event,
- // which is also the default event
+ // Fire registered action events
continueProcessing = eventDispatcher.fireActionEvents(context);
if (logger.isTraceEnabled()) {
@@ -752,6 +794,17 @@ public class ClickServlet extends HttpSe
*
* @param page page to render
* @param context the request context
+ * @throws java.lang.Exception if error occurs
+ */
+ protected void performRender(Page page, Context context) throws Exception {
+ performRender(page, context, null);
+ }
+
+ /**
+ * Performs rendering of the specified page.
+ *
+ * @param page page to render
+ * @param context the request context
* @param partial the partial response object
* @throws java.lang.Exception if error occurs
*/
@@ -804,7 +857,7 @@ public class ClickServlet extends HttpSe
}
} else if (partial != null) {
- partial.render(context);
+ renderPartial(partial, context);
} else if (page.getPath() != null) {
// Render template unless the request was a page action. This check
@@ -860,7 +913,7 @@ public class ClickServlet extends HttpSe
response.setContentType(page.getContentType());
- Writer writer = getWriter(context);
+ Writer writer = getWriter(response);
if (page.getHeaders() != null) {
setPageResponseHeaders(response, page.getHeaders());
@@ -935,6 +988,43 @@ public class ClickServlet extends HttpSe
}
/**
+ * Render the given Partial response. If the partial is null, nothing is
+ * rendered.
+ *
+ * @param partial the partial resopnse to render
+ * @param context the request context
+ */
+ protected void renderPartial(Partial partial, Context context) {
+ if (partial == null) {
+ return;
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ partial.render(context);
+
+ if (!configService.isProductionMode()) {
+ HtmlStringBuffer buffer = new HtmlStringBuffer(50);
+ if (logger.isTraceEnabled()) {
+ buffer.append(" ");
+ }
+
+ buffer.append("renderPartial (");
+ buffer.append(partial.getContentType());
+ buffer.append(")");
+ String template = partial.getTemplate();
+ if (template != null) {
+ buffer.append(": ");
+ buffer.append(template);
+ }
+ buffer.append(" - ");
+ buffer.append(System.currentTimeMillis() - startTime);
+ buffer.append(" ms");
+ logger.info(buffer);
+ }
+ }
+
+ /**
* Return a new Page instance for the given request context. This method will
* invoke {@link #initPage(String, Class, HttpServletRequest)} to create
* the Page instance and then set the properties on the page.
@@ -1010,6 +1100,11 @@ public class ClickServlet extends HttpSe
@SuppressWarnings("deprecation")
protected void processPageOnDestroy(Page page, long startTime) {
if (page.hasControls()) {
+
+ // notify callbacks of destroy event
+ // TODO check that exceptions don't unnecessarily trigger preDestroy
+ CallbackDispatcher.getThreadLocalDispatcher().processPreDestroy(page.getContext());
+
List<Control> controls = page.getControls();
for (int i = 0, size = controls.size(); i < size; i++) {
@@ -1539,7 +1634,16 @@ public class ClickServlet extends HttpSe
* @return the new ActionEventDispatcher instance
*/
protected ActionEventDispatcher createActionEventDispatcher() {
- return new ActionEventDispatcher();
+ return new ActionEventDispatcher(configService);
+ }
+
+ /**
+ * Creates and returns a new CallbackDispatcher instance.
+ *
+ * @return the new CallbackDispatcher instance
+ */
+ protected CallbackDispatcher createCallbackDispatcher() {
+ return new CallbackDispatcher(configService);
}
/**
@@ -1627,6 +1731,195 @@ public class ClickServlet extends HttpSe
return new PageImports(page);
}
+ // TODO refactor Page events into its a separate Livecycle class. This will
+ // take some of the responsibility off ClickServlet and remove code duplication
+
+ /**
+ * Process the given page events, invoking the "on" event callback methods
+ * and directing the response.
+ *
+ * @param page the page which events to process
+ * @param context the request context
+ * @throws Exception if an error occurs
+ */
+ protected void processAjaxPageEvents(Page page, Context context) throws Exception {
+
+ ActionEventDispatcher eventDispatcher = ActionEventDispatcher.getThreadLocalDispatcher();
+
+ CallbackDispatcher callbackDispatcher = CallbackDispatcher.getThreadLocalDispatcher();
+
+ // TODO Ajax requests shouldn't reach this code path
+ // Support direct access of click-error.htm
+ if (page instanceof ErrorPage) {
+ ErrorPage errorPage = (ErrorPage) page;
+ errorPage.setMode(configService.getApplicationMode());
+
+ // Notify the dispatcher and registry of the error
+ eventDispatcher.errorOccurred(errorPage.getError());
+ callbackDispatcher.errorOccurred(errorPage.getError());
+ }
+
+ boolean continueProcessing = performOnSecurityCheck(page, context);
+
+ Partial partial = null;
+ if (continueProcessing) {
+
+ // Handle page method
+ String pageAction = context.getRequestParameter(Page.PAGE_ACTION);
+ if (pageAction != null) {
+ continueProcessing = false;
+ partial = ClickUtils.invokeAction(page, pageAction);
+ callbackDispatcher.processBeforeResponse(context);
+ callbackDispatcher.processBeforeGetHeadElements(context);
+
+ renderPartial(partial, context);
+ }
+ }
+
+ if (continueProcessing) {
+ performOnInit(page, context);
+
+ // TODO: Ajax doesn't support forward. Is it still necessary to
+ // check isForward?
+ if (callbackDispatcher.hasBehaviorEnabledControls() && !context.isForward()) {
+
+ // Perform onProcess for regsitered Ajax controls
+ processAjaxControls(context, eventDispatcher, callbackDispatcher);
+
+ // Fire behaviors registered during the onProcess event
+ // The target behavior will set the eventDispatcher partial instance
+ // to render
+ eventDispatcher.fireBehaviors(context);
+
+ // Ensure we execute the beforeResponse and beforeGetHeadElements
+ // for Ajax requests
+ callbackDispatcher.processBeforeResponse(context);
+ callbackDispatcher.processBeforeGetHeadElements(context);
+
+ partial = eventDispatcher.getPartial();
+
+ // Render the partial
+ renderPartial(partial, context);
+
+ } else {
+
+ continueProcessing = performOnProcess(page, context, eventDispatcher);
+
+ if (continueProcessing) {
+ performOnPostOrGet(page, context, context.isPost());
+
+ performOnRender(page, context);
+ }
+
+ callbackDispatcher.processBeforeResponse(context);
+ callbackDispatcher.processBeforeGetHeadElements(context);
+ performRender(page, context);
+ }
+ } else {
+ // If security check fails for an Ajax request, Click returns without
+ // any rendering. It is up to the user to render a Partial response
+ // in the onSecurityCheck event
+ // Note: this code path is also followed if a pageAction is invoked
+ }
+ }
+
+ /**
+ * Process all ajax controls and return true if the page should continue
+ * processing, false otherwise.
+ *
+ * @param context the request context
+ * @param callbackDispatcher the callback dispatcher
+ * @return true if the page should continue processing, false otherwise
+ */
+ protected boolean processAjaxControls(Context context,
+ ActionEventDispatcher eventDispatcher, CallbackDispatcher callbackDispatcher) {
+
+ boolean continueProcessing = true;
+
+ Control ajaxTarget = null;
+
+ for (Control control : callbackDispatcher.getBehaviorEnabledControls()) {
+
+ if (control.isAjaxTarget(context)) {
+ ajaxTarget = control;
+ // The first matching control will be processed. Multiple matching
+ // controls are not supported
+ break;
+ }
+ }
+
+ if (ajaxTarget != null) {
+ if (logger.isTraceEnabled()) {
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ buffer.append(" invoked: '");
+ buffer.append(ajaxTarget.getName()).append("' ");
+ String className = ClassUtils.getShortClassName(ajaxTarget.getClass());
+ buffer.append(className);
+ buffer.append(".isAjaxTarget() : true (Ajax Control found)");
+ logger.trace(buffer.toString());
+ }
+
+ // Process the control
+ if (!ajaxTarget.onProcess()) {
+ continueProcessing = false;
+ }
+
+ // Log a trace if no behavior was registered after processing the control
+ if (logger.isTraceEnabled()) {
+
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ String controlClassName = ClassUtils.getShortClassName(ajaxTarget.getClass());
+ buffer.append(" invoked: '");
+ buffer.append(ajaxTarget.getName());
+ buffer.append("' ").append(controlClassName);
+ buffer.append(".onProcess() : ").append(continueProcessing);
+ logger.trace(buffer.toString());
+
+ if (!eventDispatcher.hasBehaviorSourceSet()) {
+ logger.trace(" *no* behavior was registered while processing control");
+ }
+ }
+ } else {
+
+ if (logger.isTraceEnabled()) {
+ String msg = " *no* target control was found for Ajax request";
+ logger.trace(msg);
+ }
+ }
+
+ return continueProcessing;
+ }
+
+ protected void handleAjaxException(HttpServletRequest request,
+ HttpServletResponse response, boolean isPost, Throwable exception,
+ Class<? extends Page> pageClass) {
+
+ // If an exception occurs during an Ajax request, stream
+ // the exception instead of creating an ErrorPage
+ try {
+
+ PrintWriter writer = null;
+
+ try {
+ writer = getPrintWriter(response);
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+
+ // TODO: use return an ErrorReport instance instead?
+ writer.write("<div>\n");
+ exception.printStackTrace(writer);
+ writer.write("\n</div>");
+ } finally {
+ if (writer != null) {
+ writer.flush();
+ }
+ }
+ } catch (Throwable error) {
+ logger.error(error.getMessage(), error);
+ throw new RuntimeException(error);
+ }
+ logger.error("handleException: ", exception);
+ }
+
// ------------------------------------------------ Package Private Methods
/**
@@ -1773,22 +2066,56 @@ public class ClickServlet extends HttpSe
}
/**
+ * TODO turn into utility method?
* Retrieve a writer instance for the given context.
*
- * @param context the request context
+ * @param response the servlet response
* @return a writer instance
* @throws IOException if an input or output exception occurred
*/
- Writer getWriter(Context context) throws IOException {
+ Writer getWriter(HttpServletResponse response) throws IOException {
try {
- return context.getResponse().getWriter();
+ return response.getWriter();
} catch (IllegalStateException ignore) {
// If writer cannot be retrieved fallback to OutputStream. CLK-644
- return new OutputStreamWriter(context.getResponse().getOutputStream(),
- context.getResponse().getCharacterEncoding());
+ return new OutputStreamWriter(response.getOutputStream(),
+ response.getCharacterEncoding());
+ }
+ }
+
+ /**
+ * Return a PrintWriter instance for the given response.
+ *
+ * @param response the servlet response
+ * @return a PrintWriter instance
+ */
+ PrintWriter getPrintWriter(HttpServletResponse response) throws IOException {
+ Writer writer = getWriter(response);
+ if (writer instanceof PrintWriter) {
+ return (PrintWriter) writer;
+ }
+ return new PrintWriter(writer);
+ }
+
+ /**
+ * Return true if this is an ajax request, false otherwise.
+ *
+ * @param request the servlet request
+ * @return true if this is an ajax request, false otherwise
+ */
+ boolean isAjaxRequest(HttpServletRequest request) {
+ boolean isAjaxRequest = false;
+ if (Context.hasThreadLocalContext()) {
+ Context context = Context.getThreadLocalContext();
+ if (context.isAjaxRequest()) {
+ isAjaxRequest = true;
+ }
+ } else {
+ isAjaxRequest = ClickUtils.isAjaxRequest(request);
}
+ return isAjaxRequest;
}
// ---------------------------------------------------------- Inner Classes