+ * The following example shows how to register an ActionListener with a custom
+ * Control:
+ *
+ *
+ * public class MyControl extends AbstractControl {
+ * ...
+ *
+ * public boolean onProcess() {
+ * bindRequestValue();
+ *
+ * if (isClicked()) {
+ * // Dispatch an action listener event for invocation after
+ * // control processing has finished
+ * dispatchActionEvent();
+ * }
+ *
+ * return true;
+ * }
+ * }
+ *
+ * When the link is clicked it invokes the method
+ * {@link org.openidentityplatform.openam.click.control.AbstractControl#dispatchActionEvent()}.
+ * 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
+ * onProcess() method have been invoked.
+ */
+public class ActionEventDispatcher {
+
+ // Constants --------------------------------------------------------------
+
+ /** The thread local dispatcher holder. */
+ private static final ThreadLocal THREAD_LOCAL_DISPATCHER_STACK
+ = new ThreadLocal<>();
+
+ // Variables --------------------------------------------------------------
+
+ /** The list of registered event sources. */
+ List eventSourceList;
+
+ /** The list of registered event listeners. */
+ List eventListenerList;
+
+ /** The set of Controls with attached AjaxBehaviors. */
+ Set ajaxBehaviorSourceSet;
+
+ /**
+ * The {@link org.apache.click.ActionResult} to render. This action result is
+ * returned from the target Behavior.
+ */
+ ActionResult actionResult;
+
+ /** The application log service. */
+ LogService logger;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * Construct the ActionEventDispatcher with the given ConfigService.
+ *
+ * @param configService the click application configuration service
+ */
+ public ActionEventDispatcher(ConfigService configService) {
+ this.logger = configService.getLogService();
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * Register the event source and event ActionListener to be fired by the
+ * ClickServlet once all the controls have been processed.
+ *
+ * @param source the action event source
+ * @param listener the event action listener
+ */
+ public static void dispatchActionEvent(Control source, ActionListener listener) {
+ Validate.notNull(source, "Null source parameter");
+ Validate.notNull(listener, "Null listener parameter");
+
+ ActionEventDispatcher instance = getThreadLocalDispatcher();
+ instance.registerActionEvent(source, listener);
+ }
+
+ /**
+ * Register the source control which AjaxBehaviors should be fired by the
+ * ClickServlet.
+ *
+ * @param source the source control which behaviors should be fired
+ */
+ public static void dispatchAjaxBehaviors(Control source) {
+ Validate.notNull(source, "Null source parameter");
+
+ ActionEventDispatcher instance = getThreadLocalDispatcher();
+ instance.registerAjaxBehaviorSource(source);
+ }
+
+ /**
+ * Return the thread local ActionEventDispatcher instance.
+ *
+ * @return the thread local ActionEventDispatcher instance.
+ * @throws RuntimeException if an ActionEventDispatcher is not available on
+ * the thread
+ */
+ public static ActionEventDispatcher getThreadLocalDispatcher() {
+ return getDispatcherStack().peek();
+ }
+
+ /**
+ * Returns true if an ActionEventDispatcher instance is available on the
+ * current thread, false otherwise.
+ *
+ * Unlike {@link #getThreadLocalDispatcher()} this method can safely be used
+ * and will not throw an exception if an ActionEventDispatcher is not
+ * available on the current thread.
+ *
+ * @return true if an ActionEventDispatcher instance is available on the
+ * current thread, false otherwise
+ */
+ public static boolean hasThreadLocalDispatcher() {
+ DispatcherStack dispatcherStack = THREAD_LOCAL_DISPATCHER_STACK.get();
+ if (dispatcherStack == null) {
+ return false;
+ }
+ return !dispatcherStack.isEmpty();
+ }
+
+ /**
+ * Fire all the registered action events after the Page Controls have been
+ * processed and return true if the page should continue processing.
+ *
+ * @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());
+ }
+
+ /**
+ * Fire all the registered AjaxBehaviors and return true if the page should
+ * continue processing, false otherwise.
+ *
+ * @see #fireAjaxBehaviors(Context, java.util.Set)
+ *
+ * @param context the request context
+ *
+ * @return true if the page should continue processing, false otherwise
+ */
+ public boolean fireAjaxBehaviors(Context context) {
+
+ if (!hasAjaxBehaviorSourceSet()) {
+ return true;
+ }
+
+ return fireAjaxBehaviors(context, getAjaxBehaviorSourceSet());
+ }
+
+ // Protected Methods ------------------------------------------------------
+
+ /**
+ * Allow the dispatcher to handle the error that occurred.
+ *
+ * @param throwable the error which occurred during processing
+ */
+ protected void errorOccurred(Throwable throwable) {
+ // Clear the control listeners and behaviors from the dispatcher
+ clear();
+ }
+
+ /**
+ * Fire the actions for the given listener list and event source list which
+ * return true if the page should continue processing.
+ *
+ * This method can be overridden if you need to customize the way events
+ * are fired.
+ *
+ * @param context the request context
+ * @param eventSourceList the list of source controls
+ * @param eventListenerList the list of listeners to fire
+ *
+ * @return true if the page should continue processing or false otherwise
+ */
+ 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.remove(0);
+ ActionListener listener = eventListenerList.remove(0);
+
+ if (!fireActionEvent(context, source, listener)) {
+ continueProcessing = false;
+ }
+ }
+
+ return continueProcessing;
+ }
+
+ /**
+ * Fire the action for the given listener and event source which
+ * return true if the page should continue processing.
+ *
+ * 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, false otherwise
+ */
+ protected boolean fireActionEvent(Context context, Control source,
+ ActionListener listener) {
+ return listener.onAction(source);
+ }
+
+ /**
+ * Fire the AjaxBehaviors for the given control set and return true if the page
+ * should continue processing, false otherwise.
+ *
+ * This method can be overridden if you need to customize the way
+ * AjaxBehaviors are fired.
+ *
+ * @see #fireAjaxBehaviors(Context, Control)
+ *
+ * @param context the request context
+ * @param ajaxBbehaviorSourceSet the set of controls with attached AjaxBehaviors
+ *
+ * @return true if the page should continue processing, false otherwise
+ */
+ protected boolean fireAjaxBehaviors(Context context, Set ajaxBbehaviorSourceSet) {
+
+ boolean continueProcessing = true;
+
+ for (Iterator it = ajaxBbehaviorSourceSet.iterator(); it.hasNext();) {
+ Control source = it.next();
+
+ // Pop the first entry in the set
+ it.remove();
+
+ if (!fireAjaxBehaviors(context, source)) {
+ continueProcessing = false;
+ }
+ }
+
+ return continueProcessing;
+ }
+
+ /**
+ * Fire the AjaxBehaviors for the given control and return true if the
+ * page should continue processing, false otherwise. AjaxBehaviors will
+ * only fire if their {@link org.openidentityplatform.openam.click.ajax.AjaxBehavior#isAjaxTarget(Context) isAjaxTarget()}
+ * method returns true.
+ *
+ * This method can be overridden if you need to customize the way
+ * AjaxBehaviors are fired.
+ *
+ * @param context the request context
+ * @param source the control which attached behaviors should be fired
+ *
+ * @return true if the page should continue processing, false otherwise
+ */
+ protected boolean fireAjaxBehaviors(Context context, Control source) {
+
+ boolean continueProcessing = true;
+
+ if (logger.isTraceEnabled()) {
+ String sourceClassName = ClassUtils.getShortClassName(source.getClass());
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ buffer.append(" processing AjaxBehaviors for control: '");
+ buffer.append(source.getName()).append("' ");
+ buffer.append(sourceClassName);
+ logger.trace(buffer.toString());
+ }
+
+ for (Behavior behavior : source.getBehaviors()) {
+
+ if (behavior instanceof AjaxBehavior) {
+ AjaxBehavior ajaxBehavior = (AjaxBehavior) behavior;
+
+ boolean isAjaxTarget = ajaxBehavior.isAjaxTarget(context);
+
+ if (logger.isTraceEnabled()) {
+ String behaviorClassName = ClassUtils.getShortClassName(
+ ajaxBehavior.getClass());
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ buffer.append(" invoked: ");
+ buffer.append(behaviorClassName);
+ buffer.append(".isAjaxTarget() : ");
+ buffer.append(isAjaxTarget);
+ logger.trace(buffer.toString());
+ }
+
+ if (isAjaxTarget) {
+
+ // The first non-null ActionResult returned will be rendered, other
+ // ActionResult instances are ignored
+ ActionResult behaviorActionResult =
+ ajaxBehavior.onAction(source);
+ if (actionResult == null && behaviorActionResult != null) {
+ actionResult = behaviorActionResult;
+ }
+
+ if (logger.isTraceEnabled()) {
+ String behaviorClassName = ClassUtils.getShortClassName(
+ ajaxBehavior.getClass());
+ String actionResultClassName = null;
+
+ if (behaviorActionResult != null) {
+ actionResultClassName = ClassUtils.getShortClassName(
+ behaviorActionResult.getClass());
+ }
+
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ buffer.append(" invoked: ");
+ buffer.append(behaviorClassName);
+ buffer.append(".onAction() : ");
+ buffer.append(actionResultClassName);
+
+ if (actionResult == behaviorActionResult
+ && behaviorActionResult != null) {
+ buffer.append(" (ActionResult will be rendered)");
+ } else {
+ if (behaviorActionResult == null) {
+ buffer.append(" (ActionResult is null and will be ignored)");
+ } else {
+ buffer.append(" (ActionResult will be ignored since another AjaxBehavior already retuned a non-null ActionResult)");
+ }
+ }
+
+ logger.trace(buffer.toString());
+ }
+
+ continueProcessing = false;
+ break;
+ }
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+
+ // continueProcessing is true if no AjaxBehavior was the target
+ // of the request
+ if (continueProcessing) {
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ String sourceClassName = ClassUtils.getShortClassName(
+ source.getClass());
+ buffer.append(" *no* target AjaxBehavior found for '");
+ buffer.append(source.getName()).append("' ");
+ buffer.append(sourceClassName);
+ buffer.append(" - invoking AjaxBehavior.isAjaxTarget() returned false for all AjaxBehaviors");
+ logger.trace(buffer.toString());
+ }
+ }
+
+ // Ajax requests stops further processing
+ return continueProcessing;
+ }
+
+ // Package Private Methods ------------------------------------------------
+
+ /**
+ * Register the event source and event ActionListener.
+ *
+ * @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 AjaxBehavior source control.
+ *
+ * @param source the AjaxBehavior source control
+ */
+ void registerAjaxBehaviorSource(Control source) {
+ Validate.notNull(source, "Null source parameter");
+
+ getAjaxBehaviorSourceSet().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;
+ }
+
+ /**
+ * Return the list of event listeners.
+ *
+ * @return list of event listeners
+ */
+ List getEventListenerList() {
+ if (eventListenerList == null) {
+ eventListenerList = new ArrayList();
+ }
+ return eventListenerList;
+ }
+
+ /**
+ * Return the list of event sources.
+ *
+ * @return list of event sources
+ */
+ List getEventSourceList() {
+ if (eventSourceList == null) {
+ eventSourceList = new ArrayList();
+ }
+ return eventSourceList;
+ }
+
+ /**
+ * Clear the events and behaviors.
+ */
+ void clear() {
+ if (hasActionEvents()) {
+ getEventSourceList().clear();
+ getEventListenerList().clear();
+ }
+
+ if (hasAjaxBehaviorSourceSet()) {
+ getAjaxBehaviorSourceSet().clear();
+ }
+ }
+
+ /**
+ * Return the Behavior's action result or null if no behavior was dispatched.
+ *
+ * @return the Behavior's action result or null if no behavior was dispatched
+ */
+ ActionResult getActionResult() {
+ return actionResult;
+ }
+
+ /**
+ * Return true if a control with AjaxBehaviors was registered, false otherwise.
+ *
+ * @return true if a control with AjaxBehaviors was registered, false otherwise.
+ */
+ boolean hasAjaxBehaviorSourceSet() {
+ if (ajaxBehaviorSourceSet == null || ajaxBehaviorSourceSet.isEmpty()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return the set of controls with attached AjaxBehaviors.
+ *
+ * @return set of control with attached AjaxBehaviors
+ */
+ Set getAjaxBehaviorSourceSet() {
+ if (ajaxBehaviorSourceSet == null) {
+ ajaxBehaviorSourceSet = new LinkedHashSet();
+ }
+ return ajaxBehaviorSourceSet;
+ }
+
+ /**
+ * Adds the specified ActionEventDispatcher on top of the dispatcher stack.
+ *
+ * @param actionEventDispatcher the ActionEventDispatcher to add
+ */
+ static void pushThreadLocalDispatcher(ActionEventDispatcher actionEventDispatcher) {
+ getDispatcherStack().push(actionEventDispatcher);
+ }
+
+ /**
+ * Remove and return the actionEventDispatcher instance on top of the
+ * dispatcher stack.
+ *
+ * @return the actionEventDispatcher instance on top of the dispatcher stack
+ */
+ static ActionEventDispatcher popThreadLocalDispatcher() {
+ DispatcherStack dispatcherStack = getDispatcherStack();
+ ActionEventDispatcher actionEventDispatcher = dispatcherStack.pop();
+
+ if (dispatcherStack.isEmpty()) {
+ THREAD_LOCAL_DISPATCHER_STACK.set(null);
+ }
+
+ return actionEventDispatcher;
+ }
+
+ /**
+ * Return the stack data structure where ActionEventDispatchers are stored.
+ *
+ * @return stack data structure where ActionEventDispatcher are stored
+ */
+ static ActionEventDispatcher.DispatcherStack getDispatcherStack() {
+ DispatcherStack dispatcherStack = THREAD_LOCAL_DISPATCHER_STACK.get();
+
+ if (dispatcherStack == null) {
+ dispatcherStack = new ActionEventDispatcher.DispatcherStack(2);
+ THREAD_LOCAL_DISPATCHER_STACK.set(dispatcherStack);
+ }
+
+ return dispatcherStack;
+ }
+
+ // Inner Classes ----------------------------------------------------------
+
+ /**
+ * 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 ActionEventDispatcher onto the top of this stack.
+ *
+ * @param actionEventDispatcher the ActionEventDispatcher to push onto this stack
+ * @return the ActionEventDispatcher pushed on this stack
+ */
+ private ActionEventDispatcher push(ActionEventDispatcher actionEventDispatcher) {
+ add(actionEventDispatcher);
+
+ return actionEventDispatcher;
+ }
+
+ /**
+ * Removes and return the ActionEventDispatcher at the top of this stack.
+ *
+ * @return the ActionEventDispatcher at the top of this stack
+ */
+ private ActionEventDispatcher pop() {
+ ActionEventDispatcher actionEventDispatcher = peek();
+
+ remove(size() - 1);
+
+ return actionEventDispatcher;
+ }
+
+ /**
+ * Looks at the ActionEventDispatcher at the top of this stack without
+ * removing it.
+ *
+ * @return the ActionEventDispatcher at the top of this stack
+ */
+ private ActionEventDispatcher peek() {
+ int length = size();
+
+ if (length == 0) {
+ String msg = "No ActionEventDispatcher available on ThreadLocal Dispatcher Stack";
+ throw new RuntimeException(msg);
+ }
+
+ return get(length - 1);
+ }
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/ActionListener.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/ActionListener.java
new file mode 100644
index 0000000000..a7ca79b482
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/ActionListener.java
@@ -0,0 +1,72 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+import java.io.Serializable;
+
+/**
+ * Provides a listener interface for receiving control action events.
+ * The usage model is similar to the java.awt.event.ActionListener
+ * interface.
+ *
+ * The class that is interested in processing an action event
+ * implements this interface, and the object created with that class is
+ * registered with a control, using the control's setActionListener
+ * method. When the action event occurs, that object's onAction method
+ * is invoked.
+ *
+ *
Listener Example
+ *
+ * An ActionListener example is provided below:
+ *
+ *
+ */
+public interface ActionListener extends Serializable {
+
+ /**
+ * Return true if the control and page processing should continue, or false
+ * otherwise.
+ *
+ * @param source the source of the action event
+ * @return true if control and page processing should continue or false
+ * otherwise.
+ */
+ public boolean onAction(Control source);
+
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/ActionResult.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/ActionResult.java
new file mode 100644
index 0000000000..60ac4b9c70
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/ActionResult.java
@@ -0,0 +1,580 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.openidentityplatform.openam.click.util.ClickUtils;
+
+/**
+ * Provides an ActionResult that is returned by Page Actions and AjaxBehaviors.
+ * ActionResults are often used to return a partial response to the browser
+ * instead of the full page content.
+ *
+ * An ActionResult can consist of a String (HTML, JSON, XML, plain text) or a byte
+ * array (jpg, gif, png, pdf or excel documents). The ActionResult {@link #contentType}
+ * must be set appropriately in order for the browser to recognize the action result.
+ *
+ * ActionResults are returned by {@link org.apache.click.ajax.AjaxBehavior Ajax Behaviors}
+ * and Page Action methods.
+ *
+ *
Ajax Behavior
+ *
+ * Ajax requests are handled by adding an {@link org.apache.click.ajax.AjaxBehavior Ajax Behavior}
+ * to a control. The AjaxBehavior {@link org.apache.click.ajax.AjaxBehavior#onAction(org.apache.click.Control) onAction}
+ * method will handle the request and return a ActionResult instance that contains
+ * the response, thus bypassing the rendering of the Page template. For example:
+ *
+ *
+ * private ActionLink link = new ActionLink("link");
+ *
+ * public void onInit() {
+ * addControl(link);
+ *
+ * link.addBehavior(new AjaxBehavior() {
+ *
+ * // The onAction method must return a ActionResult
+ * public ActionResult onAction(Control source) {
+ * // Create a new action result containing an HTML snippet and HTML content type
+ * ActionResult actionResult = new ActionResult("<span>Hello World</span>", ActionResult.HTML);
+ * return actionResult;
+ * }
+ * });
+ * }
+ *
+ *
Page Action
+ *
+ * A Page Action is a method on a Page that can be invoked directly
+ * from the browser. The Page Action method returns an ActionResult instance that
+ * is rendered to the browser, thus bypassing the rendering of the Page template.
+ *
+ *
+ * private ActionLink link = new ActionLink("link");
+ *
+ * public void onInit() {
+ * link.addControl(link);
+ *
+ * // A "pageAction" is set as a parameter on the link. The "pageAction"
+ * // value is set to the Page method: "renderHelloWorld"
+ * link.setParameter(PAGE_ACTION, "renderHelloWorld");
+ * }
+ *
+ * /**
+ * * This is a "pageAction" method that will render an HTML response.
+ * *
+ * * Note the signature of the pageAction: a public, no-argument method
+ * * returning a ActionResult instance.
+ * */
+ * public ActionResult renderHelloWorld() {
+ * ActionResult actionResult = new ActionResult("<span>Hello World</span>", ActionResult.HTML);
+ * return actionResult;
+ * }
+ *
+ *
Content Type
+ *
+ * The {@link #contentType} of the ActionResult must be set to the appropriate type
+ * in order for the client to recognize the response. ActionResult provides constants
+ * for some of the common content types, including: {@link #XML text/xml},
+ * {@link #HTML text/html}, {@link #JSON application/json}, {@link #TEXT text/plain}.
+ *
+ * For example:
+ *
+ * ActionResult actionResult = new ActionResult("alert('hello world');", ActionResult.JAVASCRIPT);
+ *
+ * ...
+ *
+ * // content type can also be set through the setContentType method
+ * actionResult.setContentType(ActionResult.JAVASCRIPT);
+ *
+ * ...
+ *
+ *
+ * More content types can be retrieved through {@link org.apache.click.util.ClickUtils#getMimeType(java.lang.String)}:
+ *
+ * // lookup content type for PNG
+ * String contentType = ClickUtils.getMimeType("png");
+ * actionResult.setContentType(contentType);
+ */
+public class ActionResult {
+
+ // Constants --------------------------------------------------------------
+
+ /** The plain text content type constant: text/plain. */
+ public static final String TEXT = "text/plain";
+
+ /** The html content type constant: text/html. */
+ public static final String HTML = "text/html";
+
+ /** The The xhtml content type constant: application/xhtml+xml. */
+ public static final String XHTML = "application/xhtml+xml";
+
+ /** The json content type constant: text/json. */
+ public static final String JSON = "application/json";
+
+ /** The javascript content type constant: text/javascript. */
+ public static final String JAVASCRIPT = "text/javascript";
+
+ /** The xml content type constant: text/xml. */
+ public static final String XML = "text/xml";
+
+ /** The ActionResult writer buffer size. */
+ private static final int WRITER_BUFFER_SIZE = 256; // For text, set a small response size
+
+ /** The ActionResult output buffer size. */
+ private static final int OUTPUT_BUFFER_SIZE = 4 * 1024; // For binary, set a a large response size
+
+ // Variables --------------------------------------------------------------
+
+ /** The content to render. */
+ private String content;
+
+ /** The content as a byte array. */
+ private byte[] bytes;
+
+ /** The servlet response reader. */
+ private Reader reader;
+
+ /** The servlet response input stream. */
+ private InputStream inputStream;
+
+ /** The response content type. */
+ private String contentType;
+
+ /** The response character encoding. */
+ private String characterEncoding;
+
+ /** Indicates whether the ActionResult should be cached by browser. */
+ private boolean cacheActionResult = false;
+
+ /** The path of the actionResult template to render. */
+ private String template;
+
+ /** The model for the ActionResult {@link #template}. */
+ private Map model;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * Construct the ActionResult for the given template and model.
+ *
+ * When the ActionResult is rendered the template and model will be merged and
+ * the result will be streamed back to the client.
+ *
+ * For example:
+ *
+ * public class MyPage extends Page {
+ * public void onInit() {
+ *
+ * Behavior behavior = new DefaultAjaxBehavior() {
+ *
+ * public ActionResult onAction() {
+ *
+ * Map model = new HashMap();
+ * model.put("id", "link");
+ *
+ * // Note: we set XML as the content type
+ * ActionResult actionResult = new ActionResult("/js/actionResult.xml", model, ActionResult.XML);
+ *
+ * return actionResult;
+ * }
+ * }
+ * }
+ * }
+ *
+ * @param template the template to render and stream back to the client
+ * @param model the template data model
+ * @param contentType the response content type
+ */
+ public ActionResult(String template, Map model, String contentType) {
+ this.template = template;
+ this.model = model;
+ this.contentType = contentType;
+ }
+
+ /**
+ * Construct the ActionResult for the given reader and content type.
+ *
+ * @param reader the reader which characters must be streamed back to the
+ * client
+ * @param contentType the response content type
+ */
+ public ActionResult(Reader reader, String contentType) {
+ this.reader = reader;
+ this.contentType = contentType;
+ }
+
+ /**
+ * Construct the ActionResult for the given inputStream and content type.
+ *
+ * @param inputStream the input stream to stream back to the client
+ * @param contentType the response content type
+ */
+ public ActionResult(InputStream inputStream, String contentType) {
+ this.inputStream = inputStream;
+ this.contentType = contentType;
+ }
+
+ /**
+ * Construct the ActionResult for the given String content and content type.
+ *
+ * @param content the String content to stream back to the client
+ * @param contentType the response content type
+ */
+ public ActionResult(String content, String contentType) {
+ this.content = content;
+ this.contentType = contentType;
+ }
+
+ /**
+ * Construct the ActionResult for the given byte array and content type.
+ *
+ * @param bytes the byte array to stream back to the client
+ * @param contentType the response content type
+ */
+ public ActionResult(byte[] bytes, String contentType) {
+ this.bytes = bytes;
+ this.contentType = contentType;
+ }
+
+ /**
+ * Construct the ActionResult for the given content. The
+ * {@link jakarta.servlet.http.HttpServletResponse#setContentType(java.lang.String) response content type}
+ * will default to {@link #TEXT}, unless overridden.
+ *
+ * @param content the content to stream back to the client
+ */
+ public ActionResult(String content) {
+ this.content = content;
+ }
+
+ /**
+ * Construct a new empty ActionResult. The
+ * {@link jakarta.servlet.http.HttpServletResponse#setContentType(java.lang.String) response content type}
+ * will default to {@link #TEXT}, unless overridden.
+ */
+ public ActionResult() {
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * Set whether the action result should be cached by the client browser or
+ * not.
+ *
+ * If false, Click will set the following headers to prevent browsers
+ * from caching the result:
+ *
+ *
+ * @param cacheActionResult indicates whether the action result should be cached
+ * by the client browser or not
+ */
+ public void setCacheActionResult(boolean cacheActionResult) {
+ this.cacheActionResult = cacheActionResult;
+ }
+
+ /**
+ * Return true if the action result should be cached by the client browser,
+ * defaults to false. It is highly unlikely that you would turn action result
+ * caching on.
+ *
+ * @return true if the action result should be cached by the client browser,
+ * false otherwise
+ */
+ public boolean isCacheActionRestul() {
+ return cacheActionResult;
+ }
+
+ /**
+ * Return the action result character encoding. If no character encoding is specified
+ * the request character encoding will be used.
+ *
+ * @return the action result character encoding
+ */
+ public String getCharacterEncoding() {
+ return characterEncoding;
+ }
+
+ /**
+ * Set the action result character encoding. If no character encoding is set the
+ * request character encoding will be used.
+ *
+ * @param characterEncoding the action result character encoding
+ */
+ public void setCharacterEncoding(String characterEncoding) {
+ this.characterEncoding = characterEncoding;
+ }
+
+ /**
+ * Set the action result response content type. If no content type is set it will
+ * default to {@value #TEXT}.
+ *
+ * @param contentType the action result response content type
+ */
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ /**
+ * Return the action result content type, default is {@value #TEXT}.
+ *
+ * @return the response content type
+ */
+ public String getContentType() {
+ if (contentType == null) {
+ contentType = TEXT;
+ }
+ return contentType;
+ }
+
+ /**
+ * Set the content String to stream back to the client.
+ *
+ * @param content the content String to stream back to the client
+ */
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ /**
+ * Return the content String to stream back to the client.
+ *
+ * @return the content String to stream back to the client
+ */
+ public String getContent() {
+ return content;
+ }
+
+ /**
+ * Set the byte array to stream back to the client.
+ *
+ * @param bytes the byte array to stream back to the client
+ */
+ public void setBytes(byte[] bytes, String contentType) {
+ this.bytes = bytes;
+ this.contentType = contentType;
+ }
+
+ /**
+ * Return the byte array to stream back to the client.
+ *
+ * @return the byte array to stream back to the client
+ */
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ /**
+ * Set the content to stream back to the client.
+ *
+ * @param inputStream the inputStream to stream back to the client
+ */
+ public void setInputStream(InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ /**
+ * Return the inputStream to stream back to the client.
+ *
+ * @return the inputStream to stream back to the client
+ */
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ /**
+ * Set the reader which characters are streamed back to the client.
+ *
+ * @param reader the reader which characters are streamed back to the client.
+ */
+ public void setReader(Reader reader) {
+ this.reader = reader;
+ }
+
+ /**
+ * Return the reader which characters are streamed back to the client.
+ *
+ * @return the reader which characters are streamed back to the client.
+ */
+ public Reader getReader() {
+ return reader;
+ }
+
+ /**
+ * Return the data model for the ActionResult {@link #template}.
+ *
+ * @return the data model for the ActionResult template
+ */
+ public Map getModel() {
+ if (model == null) {
+ model = new HashMap();
+ }
+ return model;
+ }
+
+ /**
+ * Set the model of the ActionResult template to render.
+ *
+ * If the {@link #template} property is set, the template and {@link #model}
+ * will be merged and the result will be streamed back to the client.
+ *
+ * @param model the model of the template to render
+ */
+ public void setModel(Map model) {
+ this.model = model;
+ }
+
+ /**
+ * Return the template to render for this ActionResult.
+ *
+ * @return the template to render for this ActionResult
+ */
+ public String getTemplate() {
+ return template;
+ }
+
+ /**
+ * Set the template to render for this ActionResult.
+ *
+ * @param template the template to render for this ActionResult
+ */
+ public void setTemplate(String template) {
+ this.template = template;
+ }
+
+ /**
+ * Render the ActionResult to the client.
+ *
+ * @param context the request context
+ */
+ public final void render(Context context) {
+ prepare(context);
+ renderActionResult(context);
+ }
+
+ // Protected Methods ------------------------------------------------------
+
+ /**
+ * Render the ActionResult to the client. This method can be overridden
+ * by subclasses if custom rendering or direct access to the
+ * HttpServletResponse is required.
+ *
+ * @param context the request context
+ */
+ protected void renderActionResult(Context context) {
+
+ HttpServletResponse response = context.getResponse();
+
+ Reader localReader = getReader();
+ InputStream localInputStream = getInputStream();
+
+ try {
+ String localContent = getContent();
+ byte[] localBytes = getBytes();
+
+ String localTemplate = getTemplate();
+ if (localTemplate != null) {
+ Map templateModel = getModel();
+ if (templateModel == null) {
+ templateModel = new HashMap();
+ }
+ String result = context.renderTemplate(localTemplate, templateModel);
+ localReader = new StringReader(result);
+
+ } else if (localContent != null) {
+ localReader = new StringReader(localContent);
+ } else if (localBytes != null) {
+ localInputStream = new ByteArrayInputStream(localBytes);
+ }
+
+ if (localReader != null) {
+ Writer writer = response.getWriter();
+ char[] buffer = new char[WRITER_BUFFER_SIZE];
+ int len = 0;
+ while (-1 != (len = localReader.read(buffer))) {
+ writer.write(buffer, 0, len);
+ }
+ writer.flush();
+ writer.close();
+
+ } else if (localInputStream != null) {
+ byte[] buffer = new byte[OUTPUT_BUFFER_SIZE];
+ int len = 0;
+ OutputStream outputStream = response.getOutputStream();
+ while (-1 != (len = localInputStream.read(buffer))) {
+ outputStream.write(buffer, 0, len);
+ }
+ outputStream.flush();
+ outputStream.close();
+ }
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+
+ } finally {
+ ClickUtils.close(localInputStream);
+ ClickUtils.close(localReader);
+ }
+ }
+
+ // Private Methods --------------------------------------------------------
+
+ /**
+ * Prepare the ActionResult for rendering.
+ *
+ * @param context the request context
+ */
+ private void prepare(Context context) {
+ HttpServletResponse response = context.getResponse();
+
+ if (!isCacheActionRestul()) {
+ // Set headers to disable cache
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
+ response.setDateHeader("Expires", new Date(1L).getTime());
+ }
+
+ String localContentType = getContentType();
+
+ if (getCharacterEncoding() == null) {
+
+ // Fallback to request character encoding
+ if (context.getRequest().getCharacterEncoding() != null) {
+ response.setContentType(localContentType + "; charset="
+ + context.getRequest().getCharacterEncoding());
+ } else {
+ response.setContentType(localContentType);
+ }
+
+ } else {
+ response.setContentType(localContentType + "; charset=" + getCharacterEncoding());
+ }
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/Behavior.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/Behavior.java
new file mode 100644
index 0000000000..58ac8527e0
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/Behavior.java
@@ -0,0 +1,74 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+/**
+ * Behaviors provide a mechanism for changing how Controls behave at runtime.
+ * Behaviors are added to a Control and provides interceptor methods to decorate
+ * and enhance the source Control.
+ *
+ * Behaviors provide interceptor methods for specific Control life cycle events.
+ * These interceptor methods can be implemented to further process and decorate
+ * the control or its children.
+ *
+ * The following interceptor methods are defined:
+ *
+ *
+ *
preResponse - occurs before the control markup is written to the response
+ *
preRenderHeadElements - occurs after preResponse but before the control
+ * {@link Control#getHeadElements() HEAD elements} are written to the response
+ *
preDestroy - occurs before the Control {@link Control#onDestroy() onDestroy}
+ * event handler.
+ *
+ *
+ * These interceptor methods allow the Behavior to decorate a control,
+ * for example:
+ *
+ *
+ *
add or remove Control HEAD elements such as JavaScript and CSS dependencies
+ * and setup scripts
+ *
add or remove Control attributes such as "class", "style" etc.
+ *
+ */
+public interface Behavior {
+
+ /**
+ * This event occurs before the markup is written to the HttpServletResponse.
+ *
+ * @param source the control the behavior is registered with
+ */
+ public void preResponse(Control source);
+
+ /**
+ * This event occurs after {@link #preResponse(Control)},
+ * but before the Control's {@link Control#getHeadElements()} is called.
+ *
+ * @param source the control the behavior is registered with
+ */
+ public void preRenderHeadElements(Control source);
+
+ /**
+ * This event occurs before the Control {@link Control#onDestroy() onDestroy}
+ * event handler. This event allows the behavior to cleanup or store Control
+ * state in the Session.
+ *
+ * @param source the control the behavior is registered with
+ */
+ public void preDestroy(Control source);
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickRequestWrapper.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickRequestWrapper.java
new file mode 100644
index 0000000000..482c832c77
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickRequestWrapper.java
@@ -0,0 +1,288 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+
+import org.openidentityplatform.openam.click.service.FileUploadService;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileUploadException;
+
+/**
+ * Provides a custom HttpServletRequest class for shielding users from
+ * multipart request parameters. Thus calling request.getParameter(String)
+ * will still work properly.
+ */
+class ClickRequestWrapper extends HttpServletRequestWrapper {
+
+ /**
+ * The FileItem objects for "multipart" POST requests.
+ */
+ private final Map fileItemMap;
+
+ /** The request is a multi-part file upload POST request. */
+ private final boolean isMultipartRequest;
+
+ /** The map of "multipart" request parameter values. */
+ private final Map multipartParameterMap;
+
+ /** The wrapped servlet request. */
+ private final HttpServletRequest request;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * @see HttpServletRequestWrapper(HttpServletRequest)
+ */
+ ClickRequestWrapper(final HttpServletRequest request,
+ final FileUploadService fileUploadService) {
+ super(request);
+
+ this.isMultipartRequest = ClickUtils.isMultipartRequest(request);
+ this.request = request;
+
+ if (isMultipartRequest) {
+
+ Map requestParams = new HashMap();
+ Map fileItems = new HashMap();
+
+ try {
+ List itemsList = new ArrayList();
+
+ try {
+
+ itemsList = fileUploadService.parseRequest(request);
+
+ } catch (FileUploadException fue) {
+ request.setAttribute(FileUploadService.UPLOAD_EXCEPTION, fue);
+ }
+
+ for (FileItem fileItem : itemsList) {
+ String name = fileItem.getFieldName();
+ String value = null;
+
+ // Form fields are placed in the request parameter map,
+ // while file uploads are placed in the file item map.
+ if (fileItem.isFormField()) {
+
+ if (request.getCharacterEncoding() == null) {
+ value = fileItem.getString();
+
+ } else {
+ try {
+ value = fileItem.getString(request.getCharacterEncoding());
+
+ } catch (UnsupportedEncodingException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ // Add the form field value to the parameters.
+ addToMapAsString(requestParams, name, value);
+
+ } else {
+ // Add the file item to the list of file items.
+ addToMapAsFileItem(fileItems, name, fileItem);
+ }
+ }
+
+ } catch (Throwable t) {
+
+ // Don't throw error here as it will break Context creation.
+ // Instead add the error as a request attribute.
+ request.setAttribute(Context.CONTEXT_FATAL_ERROR, t);
+
+ } finally {
+ fileItemMap = Collections.unmodifiableMap(fileItems);
+ multipartParameterMap = Collections.unmodifiableMap(requestParams);
+ }
+
+ } else {
+ fileItemMap = Collections.emptyMap();
+ multipartParameterMap = Collections.emptyMap();
+ }
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * Returns a map of FileItem arrays keyed on request parameter
+ * name for "multipart" POST requests (file uploads). Thus each map entry
+ * will consist of one or more FileItem objects.
+ *
+ * @return map of FileItem arrays keyed on request parameter name
+ * for "multipart" POST requests
+ */
+ public Map getFileItemMap() {
+ return fileItemMap;
+ }
+
+ /**
+ * @see jakarta.servlet.ServletRequest#getParameter(String)
+ */
+ @Override
+ public String getParameter(String name) {
+ if (isMultipartRequest) {
+ Object value = getMultipartParameterMap().get(name);
+
+ if (value instanceof String) {
+ return (String) value;
+ }
+
+ if (value instanceof String[]) {
+ String[] array = (String[]) value;
+ if (array.length >= 1) {
+ return array[0];
+ } else {
+ return null;
+ }
+ }
+
+ return (value == null ? null : value.toString());
+
+ } else {
+ return request.getParameter(name);
+ }
+ }
+
+ /**
+ * @see jakarta.servlet.ServletRequest#getParameterNames()
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public Enumeration getParameterNames() {
+ if (isMultipartRequest) {
+ return Collections.enumeration(getMultipartParameterMap().keySet());
+
+ } else {
+ return request.getParameterNames();
+ }
+ }
+
+ /**
+ * @see jakarta.servlet.ServletRequest#getParameterValues(String)
+ */
+ @Override
+ public String[] getParameterValues(String name) {
+ if (isMultipartRequest) {
+ Object values = getMultipartParameterMap().get(name);
+ if (values instanceof String) {
+ return new String[] { values.toString() };
+ }
+ if (values instanceof String[]) {
+ return (String[]) values;
+ } else {
+ return null;
+ }
+
+ } else {
+ return request.getParameterValues(name);
+ }
+ }
+
+ /**
+ * @see jakarta.servlet.ServletRequest#getParameterMap()
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public Map getParameterMap() {
+ if (isMultipartRequest) {
+ return getMultipartParameterMap();
+ } else {
+ return request.getParameterMap();
+ }
+ }
+
+ // Package Private Methods ------------------------------------------------
+
+ /**
+ * Return the map of "multipart" request parameter map.
+ *
+ * @return the "multipart" request parameter map
+ */
+ @SuppressWarnings("unchecked")
+ Map getMultipartParameterMap() {
+ if (request.getAttribute(ClickServlet.MOCK_MODE_ENABLED) == null) {
+ return multipartParameterMap;
+ } else {
+ // In mock mode return the request parameter map. This ensures
+ // calling request.setParameter(x,y) works for both normal and
+ // multipart requests.
+ return request.getParameterMap();
+ }
+ }
+
+ // Private Methods --------------------------------------------------------
+
+ /**
+ * Stores the specified value in a FileItem array in the map, under the
+ * specified name. Thus two values stored under the same name will be
+ * stored in the same array.
+ *
+ * @param map the map to add the specified name and value to
+ * @param name the name of the map key
+ * @param value the value to add to the FileItem array
+ */
+ private void addToMapAsFileItem(Map map, String name, FileItem value) {
+ FileItem[] oldValues = map.get(name);
+ FileItem[] newValues = null;
+ if (oldValues == null) {
+ newValues = new FileItem[] {value};
+ } else {
+ newValues = new FileItem[oldValues.length + 1];
+ System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);
+ newValues[oldValues.length] = value;
+ }
+ map.put(name, newValues);
+ }
+
+ /**
+ * Stores the specified value in an String array in the map, under the
+ * specified name. Thus two values stored under the same name will be
+ * stored in the same array.
+ *
+ * @param map the map to add the specified name and value to
+ * @param name the name of the map key
+ * @param value the value to add to the string array
+ */
+ private void addToMapAsString(Map map, String name, String value) {
+ String[] oldValues = map.get(name);
+ String[] newValues = null;
+ if (oldValues == null) {
+ newValues = new String[] {value};
+ } else {
+ newValues = new String[oldValues.length + 1];
+ System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);
+ newValues[oldValues.length] = value;
+ }
+ map.put(name, newValues);
+ }
+
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickServlet.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickServlet.java
new file mode 100644
index 0000000000..e8ff6b2fe8
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickServlet.java
@@ -0,0 +1,2305 @@
+/*
+ * 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.openidentityplatform.openam.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;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.UnavailableException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+import ognl.DefaultMemberAccess;
+import ognl.MemberAccess;
+import ognl.Ognl;
+import ognl.OgnlException;
+import ognl.TypeConverter;
+
+import org.openidentityplatform.openam.click.service.ConfigService;
+import org.openidentityplatform.openam.click.service.LogService;
+import org.openidentityplatform.openam.click.service.ResourceService;
+import org.apache.click.service.TemplateException;
+import org.openidentityplatform.openam.click.service.XmlConfigService;
+import org.openidentityplatform.openam.click.service.ConfigService.AutoBinding;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.openidentityplatform.openam.click.util.ErrorPage;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+import org.openidentityplatform.openam.click.util.PageImports;
+import org.apache.click.util.PropertyUtils;
+import org.apache.click.util.RequestTypeConverter;
+import org.apache.commons.lang.ClassUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Provides the Click application HttpServlet.
+ *
+ * Generally developers will simply configure the ClickServlet and
+ * will not use it directly in their code. For a Click web application to
+ * function the ClickServlet must be configured in the web
+ * application's /WEB-INF/web.xml file. A simple web application which
+ * maps all *.htm requests to a ClickServlet is provided below.
+ *
+ *
+ *
+ * By default the ClickServlet will attempt to load an application
+ * configuration file using the path: /WEB-INF/click.xml
+ *
+ *
Servlet Mapping
+ * By convention all Click page templates should have a .htm extension, and
+ * the ClickServlet should be mapped to process all *.htm URL requests. With
+ * this convention you have all the static HTML pages use a .html extension
+ * and they will not be processed as Click pages.
+ *
+ *
Load On Startup
+ * Note you should always set load-on-startup element to be 0 so the
+ * servlet is initialized when the server is started. This will prevent any
+ * delay for the first client which uses the application.
+ *
+ * The ClickServlet performs as much work as possible at startup to
+ * improve performance later on. The Click start up and caching strategy is
+ * configured with the Click application mode in the "click.xml" file.
+ * See the User Guide for information on how to configure the application mode.
+ *
+ *
ConfigService
+ *
+ * A single application {@link ConfigService} instance is created by the ClickServlet at
+ * startup. Once the ConfigService has been initialized it is stored in the
+ * ServletContext using the key {@value org.apache.click.service.ConfigService#CONTEXT_NAME}.
+ */
+public class ClickServlet extends HttpServlet {
+
+ // -------------------------------------------------------------- Constants
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The mock page reference request attribute: key:
+ * mock_page_reference.
+ *
+ * This attribute stores the each Page instance as a request attribute.
+ *
+ * Note: a page is only stored as a request attribute
+ * if the {@link #MOCK_MODE_ENABLED} attribute is set.
+ */
+ static final String MOCK_PAGE_REFERENCE = "mock_page_reference";
+
+ /**
+ * The mock mode request attribute: key:
+ * mock_mode_enabled.
+ *
+ * If this attribute is set (the value does not matter) certain features
+ * will be enabled which is needed for running Click in a mock environment.
+ */
+ static final String MOCK_MODE_ENABLED = "mock_mode_enabled";
+
+ /**
+ * The click application configuration service classname init parameter name:
+ * "config-service-class".
+ */
+ protected final static String CONFIG_SERVICE_CLASS = "config-service-class";
+
+ /**
+ * The custom TypeConverter classname as an init parameter name:
+ * &nbps; "type-converter-class".
+ */
+ protected final static String TYPE_CONVERTER_CLASS = "type-converter-class";
+
+ /**
+ * The forwarded request marker attribute: "click-forward".
+ */
+ protected final static String CLICK_FORWARD = "click-forward";
+
+ /**
+ * The Page to forward to request attribute: "click-page".
+ */
+ protected final static String FORWARD_PAGE = "forward-page";
+
+ // ----------------------------------------------------- Instance Variables
+
+ /** The click application configuration service. */
+ protected ConfigService configService;
+
+ /** The application log service. */
+ protected LogService logger;
+
+ /** The OGNL member access handler. */
+ protected MemberAccess memberAccess;
+
+ /** The application resource service. */
+ protected ResourceService resourceService;
+
+ /** The request parameters OGNL type converter. */
+ protected TypeConverter typeConverter;
+
+ /** The thread local page listeners. */
+ private static final ThreadLocal>
+ THREAD_LOCAL_INTERCEPTORS = new ThreadLocal<>();
+
+ // --------------------------------------------------------- Public Methods
+
+ /**
+ * Initialize the Click servlet and the Velocity runtime.
+ *
+ * @see jakarta.servlet.GenericServlet#init()
+ *
+ * @throws ServletException if the application configuration service could
+ * not be initialized
+ */
+ @Override
+ public void init() throws ServletException {
+
+ try {
+
+ // Create and initialize the application config service
+ configService = createConfigService(getServletContext());
+ initConfigService(getServletContext());
+ logger = configService.getLogService();
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Click " + ClickUtils.getClickVersion()
+ + " initialized in " + configService.getApplicationMode()
+ + " mode");
+ }
+
+ resourceService = configService.getResourceService();
+
+ } catch (Throwable e) {
+ // In mock mode this exception can occur if click.xml is not
+ // available.
+ if (getServletContext().getAttribute(MOCK_MODE_ENABLED) != null) {
+ return;
+ }
+
+ e.printStackTrace();
+
+ String msg = "error while initializing Click servlet; throwing "
+ + "jakarta.servlet.UnavailableException";
+
+ log(msg, e);
+
+ throw new UnavailableException(e.toString());
+ }
+ }
+
+ /**
+ * @see jakarta.servlet.GenericServlet#destroy()
+ */
+ @Override
+ public void destroy() {
+
+ try {
+
+ // Destroy the application config service
+ destroyConfigService(getServletContext());
+
+ } catch (Throwable e) {
+ // In mock mode this exception can occur if click.xml is not
+ // available.
+ if (getServletContext().getAttribute(MOCK_MODE_ENABLED) != null) {
+ return;
+ }
+
+ e.printStackTrace();
+
+ String msg = "error while destroying Click servlet, throwing "
+ + "jakarta.servlet.UnavailableException";
+
+ log(msg, e);
+
+ } finally {
+ // Dereference the application config service
+ configService = null;
+ }
+
+ super.destroy();
+ }
+
+ // ------------------------------------------------------ Protected Methods
+
+ /**
+ * Handle HTTP GET requests. This method will delegate the request to
+ * {@link #handleRequest(HttpServletRequest, HttpServletResponse, boolean)}.
+ *
+ * @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse)
+ *
+ * @param request the servlet request
+ * @param response the servlet response
+ * @throws ServletException if click app has not been initialized
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+
+ handleRequest(request, response, false);
+ }
+
+ /**
+ * Handle HTTP POST requests. This method will delegate the request to
+ * {@link #handleRequest(HttpServletRequest, HttpServletResponse, boolean)}.
+ *
+ * @see HttpServlet#doPost(HttpServletRequest, HttpServletResponse)
+ *
+ * @param request the servlet request
+ * @param response the servlet response
+ * @throws ServletException if click app has not been initialized
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doPost(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+
+ handleRequest(request, response, true);
+ }
+
+ /**
+ * Handle the given servlet request and render the results to the
+ * servlet response.
+ *
+ * If an exception occurs within this method the exception will be delegated
+ * to:
+ *
+ * {@link #handleException(HttpServletRequest, HttpServletResponse, boolean, Throwable, Class)}
+ *
+ * @param request the servlet request to process
+ * @param response the servlet response to render the results to
+ * @param isPost determines whether the request is a POST
+ * @throws IOException if resource request could not be served
+ */
+ protected void handleRequest(HttpServletRequest request,
+ HttpServletResponse response, boolean isPost) throws IOException {
+
+ // Handle requests for click resources, i.e. CSS, JS and image files
+ if (resourceService.isResourceRequest(request)) {
+ resourceService.renderResource(request, response);
+ return;
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ if (logger.isDebugEnabled()) {
+ HtmlStringBuffer buffer = new HtmlStringBuffer(200);
+ buffer.append(request.getMethod());
+ buffer.append(" ");
+ buffer.append(request.getRequestURL());
+ logger.debug(buffer);
+ }
+
+ // Handle click page requests
+ Page page = null;
+ try {
+
+ ActionEventDispatcher eventDispatcher = createActionEventDispatcher();
+ // Bind ActionEventDispatcher to current thread
+ ActionEventDispatcher.pushThreadLocalDispatcher(eventDispatcher);
+
+ ControlRegistry controlRegistry = createControlRegistry();
+ // Bind ControlRegistry to current thread
+ ControlRegistry.pushThreadLocalRegistry(controlRegistry);
+
+ Context context = createContext(request, response, isPost);
+ // Bind context to current thread
+ Context.pushThreadLocalContext(context);
+
+ // Check for fatal error that occurred while creating Context
+ Throwable error = (Throwable) request.getAttribute(Context.CONTEXT_FATAL_ERROR);
+
+ if (error != null) {
+ // Process exception through Click's exception handler.
+ if (error instanceof Exception) {
+ throw (Exception) error;
+ }
+ // Errors are not handled by Click, let the server handle it
+ if (error instanceof Error) {
+ throw (Error) error;
+ } else {
+ // Throwables are not handled by Click, let the server handle it
+ throw new RuntimeException(error);
+ }
+ }
+
+ page = createPage(context);
+
+ // If no page created, then an PageInterceptor has aborted processing
+ if (page == null) {
+ return;
+ }
+
+ if (page.isStateful()) {
+ synchronized (page) {
+ processPage(page);
+ processPageOnDestroy(page, startTime);
+ // Mark page as already destroyed for finally block
+ page = null;
+ }
+
+ } else {
+ processPage(page);
+ }
+
+ } catch (Exception e) {
+ Class extends Page> pageClass =
+ configService.getPageClass(ClickUtils.getResourcePath(request));
+
+ handleException(request, response, isPost, e, pageClass);
+
+ } catch (ExceptionInInitializerError eiie) {
+ Throwable cause = eiie.getException();
+ cause = (cause != null) ? cause : eiie;
+
+ Class extends Page> pageClass =
+ configService.getPageClass(ClickUtils.getResourcePath(request));
+
+ handleException(request, response, isPost, cause, pageClass);
+
+ } finally {
+
+ try {
+ if (page != null) {
+ if (page.isStateful()) {
+ synchronized (page) {
+ processPageOnDestroy(page, startTime);
+ }
+
+ } else {
+ processPageOnDestroy(page, startTime);
+ }
+ }
+
+ for (PageInterceptor interceptor : getThreadLocalInterceptors()) {
+ interceptor.postDestroy(page);
+ }
+
+ setThreadLocalInterceptors(null);
+
+ } finally {
+ // Only clear the context when running in normal mode.
+ if (request.getAttribute(MOCK_MODE_ENABLED) == null) {
+ Context.popThreadLocalContext();
+ }
+ ControlRegistry.popThreadLocalRegistry();
+ ActionEventDispatcher.popThreadLocalDispatcher();
+ }
+ }
+ }
+
+ /**
+ * Provides the application exception handler. The application exception
+ * will be delegated to the configured error page. The default error page is
+ * {@link ErrorPage} and the page template is "click/error.htm"
+ * Applications which wish to provide their own customized error handling
+ * must subclass ErrorPage and specify their page in the
+ * "/WEB-INF/click.xml" application configuration file. For example:
+ *
+ *
+ *
+ * This method is designed to be overridden by applications providing their
+ * own page creation patterns.
+ *
+ * A typical example of this would be with Inversion of Control (IoC)
+ * frameworks such as Spring or HiveMind. For example a Spring application
+ * could override this method and use a ApplicationContext to instantiate
+ * new Page objects:
+ *
+ *
+ * @param path the request page path
+ * @param pageClass the page Class the request is mapped to
+ * @param request the page request
+ * @return a new Page object
+ * @throws Exception if an error occurs creating the Page
+ */
+ protected Page newPageInstance(String path, Class extends Page> pageClass,
+ HttpServletRequest request) throws Exception {
+
+ return pageClass.newInstance();
+ }
+
+ /**
+ * Provides an extension point for ClickServlet sub classes to activate
+ * stateful page which may have been deserialized.
+ *
+ * This method does nothing and is designed for extension.
+ *
+ * @param page the page instance to activate
+ */
+ protected void activatePageInstance(Page page) {
+ }
+
+ /**
+ * Return a new VelocityContext for the given pages model and Context.
+ *
+ * The following values automatically added to the VelocityContext:
+ *
+ *
any public Page fields using the fields name
+ *
context - the Servlet context path, e.g. /mycorp
+ *
format - the {@link org.apache.click.util.Format} object for formatting
+ * the display of objects
+ *
imports - the {@link org.apache.click.util.PageImports} object
+ *
messages - the page messages bundle
+ *
path - the page of the page template to render
+ *
request - the pages servlet request
+ *
response - the pages servlet request
+ *
session - the {@link org.apache.click.util.SessionMap} adaptor for the
+ * users HttpSession
+ *
+ *
+ * @see org.openidentityplatform.openam.click.util.ClickUtils#createTemplateModel(Page, Context)
+ *
+ * @param page the page to create a VelocityContext for
+ * @return a new VelocityContext
+ */
+ @SuppressWarnings("deprecation")
+ protected Map createTemplateModel(final Page page) {
+
+ if (configService.getAutoBindingMode() != AutoBinding.NONE) {
+
+ processPageFields(page, new ClickServlet.FieldCallback() {
+ public void processField(String fieldName, Object fieldValue) {
+ if (fieldValue instanceof Control == false) {
+ page.addModel(fieldName, fieldValue);
+
+ } else {
+ // Add any controls not already added to model
+ Control control = (Control) fieldValue;
+ if (!page.getModel().containsKey(control.getName())) {
+ page.addControl(control);
+ }
+ }
+ }
+ });
+ }
+
+ final Context context = page.getContext();
+ final Map model = ClickUtils.createTemplateModel(page, context);
+
+ PageImports pageImports = page.getPageImports();
+ pageImports.populateTemplateModel(model);
+
+ return model;
+ }
+
+ /**
+ * Set the HTTP headers in the servlet response. The Page response headers
+ * are defined in {@link Page#getHeaders()}.
+ *
+ * @param response the response to set the headers in
+ * @param headers the map of HTTP headers to set in the response
+ */
+ protected void setPageResponseHeaders(HttpServletResponse response,
+ Map headers) {
+
+ for (Map.Entry entry : headers.entrySet()) {
+ String name = entry.getKey();
+ Object value = entry.getValue();
+
+ if (value instanceof String) {
+ String strValue = (String) value;
+ if (!strValue.equalsIgnoreCase("Content-Encoding")) {
+ response.setHeader(name, strValue);
+ }
+
+ } else if (value instanceof Date) {
+ long time = ((Date) value).getTime();
+ response.setDateHeader(name, time);
+
+ } else if (value instanceof Integer) {
+ int intValue = (Integer) value;
+ response.setIntHeader(name, intValue);
+
+ } else if (value != null) {
+ throw new IllegalStateException("Invalid Page header value type: "
+ + value.getClass() + ". Header value must of type String, Date or Integer.");
+ }
+ }
+ }
+
+ /**
+ * Set the page model, context, format, messages and path as request
+ * attributes to support JSP rendering. These request attributes include:
+ *
+ *
any public Page fields using the fields name
+ *
context - the Servlet context path, e.g. /mycorp
+ *
format - the {@link org.apache.click.util.Format} object for
+ * formatting the display of objects
+ *
forward - the page forward path, if defined
+ *
imports - the {@link org.apache.click.util.PageImports} object
+ *
messages - the page messages bundle
+ *
path - the page of the page template to render
+ *
+ *
+ * @param page the page to set the request attributes on
+ */
+ @SuppressWarnings("deprecation")
+ protected void setRequestAttributes(final Page page) {
+ final HttpServletRequest request = page.getContext().getRequest();
+
+ processPageFields(page, new ClickServlet.FieldCallback() {
+ public void processField(String fieldName, Object fieldValue) {
+ if (fieldValue instanceof Control == false) {
+ request.setAttribute(fieldName, fieldValue);
+ } else {
+ // Add any controls not already added to model
+ Control control = (Control) fieldValue;
+ if (!page.getModel().containsKey(control.getName())) {
+ page.addControl(control);
+ }
+ }
+ }
+ });
+
+ Map model = page.getModel();
+ for (Map.Entry entry : model.entrySet()) {
+ String name = entry.getKey();
+ Object value = entry.getValue();
+
+ request.setAttribute(name, value);
+ }
+
+ request.setAttribute("context", request.getContextPath());
+ if (model.containsKey("context")) {
+ String msg = page.getClass().getName() + " on " + page.getPath()
+ + " model contains an object keyed with reserved "
+ + "name \"context\". The request attribute "
+ + "has been replaced with the request "
+ + "context path";
+ logger.warn(msg);
+ }
+
+ request.setAttribute("format", page.getFormat());
+ if (model.containsKey("format")) {
+ String msg = page.getClass().getName() + " on " + page.getPath()
+ + " model contains an object keyed with reserved "
+ + "name \"format\". The request attribute "
+ + "has been replaced with the format object";
+ logger.warn(msg);
+ }
+
+ request.setAttribute("forward", page.getForward());
+ if (model.containsKey("forward")) {
+ String msg = page.getClass().getName() + " on " + page.getPath()
+ + " model contains an object keyed with reserved "
+ + "name \"forward\". The request attribute "
+ + "has been replaced with the page path";
+ logger.warn(msg);
+ }
+
+ request.setAttribute("path", page.getPath());
+ if (model.containsKey("path")) {
+ String msg = page.getClass().getName() + " on " + page.getPath()
+ + " model contains an object keyed with reserved "
+ + "name \"path\". The request attribute "
+ + "has been replaced with the page path";
+ logger.warn(msg);
+ }
+
+ request.setAttribute("messages", page.getMessages());
+ if (model.containsKey("messages")) {
+ String msg = page.getClass().getName() + " on " + page.getPath()
+ + " model contains an object keyed with reserved "
+ + "name \"messages\". The request attribute "
+ + "has been replaced with the page messages";
+ logger.warn(msg);
+ }
+
+ PageImports pageImports = page.getPageImports();
+ pageImports.populateRequest(request, model);
+ }
+
+ /**
+ * Return the request parameters OGNL TypeConverter. This method
+ * performs a lazy load of the TypeConverter object, using the classname
+ * defined in the Servlet init parameter type-converter-class,
+ * if this parameter is not defined this method will return a
+ * {@link RequestTypeConverter} instance.
+ *
+ * @return the request parameters OGNL TypeConverter
+ * @throws RuntimeException if the TypeConverter instance could not be created
+ */
+ @SuppressWarnings("unchecked")
+ protected TypeConverter getTypeConverter() throws RuntimeException {
+ if (typeConverter == null) {
+ Class extends TypeConverter> converter = RequestTypeConverter.class;
+
+ try {
+ String classname = getInitParameter(TYPE_CONVERTER_CLASS);
+ if (StringUtils.isNotBlank(classname)) {
+ converter = ClickUtils.classForName(classname);
+ }
+
+ typeConverter = converter.newInstance();
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return typeConverter;
+ }
+
+ /**
+ * Creates and returns a new Context instance for this path, class and
+ * request.
+ *
+ * The default implementation of this method simply creates a new Context
+ * instance.
+ *
+ * Subclasses can override this method to provide a custom Context.
+ *
+ * @param request the page request
+ * @param response the page response
+ * @param isPost true if this is a post request, false otherwise
+ * @return a Context instance
+ */
+ protected Context createContext(HttpServletRequest request,
+ HttpServletResponse response, boolean isPost) {
+
+ Context context = new Context(getServletContext(),
+ getServletConfig(),
+ request,
+ response,
+ isPost,
+ this);
+ return context;
+ }
+
+ /**
+ * Creates and returns a new ActionEventDispatcher instance.
+ *
+ * @return the new ActionEventDispatcher instance
+ */
+ protected ActionEventDispatcher createActionEventDispatcher() {
+ return new ActionEventDispatcher(configService);
+ }
+
+ /**
+ * Creates and returns a new ControlRegistry instance.
+ *
+ * @return the new ControlRegistry instance
+ */
+ protected ControlRegistry createControlRegistry() {
+ return new ControlRegistry(configService);
+ }
+
+ /**
+ * Creates and returns a new ErrorPage instance.
+ *
+ * This method creates the custom page as specified in click.xml,
+ * otherwise the default ErrorPage instance.
+ *
+ * Subclasses can override this method to provide custom ErrorPages tailored
+ * for specific exceptions.
+ *
+ * Note you can safely use {@link Context} in this
+ * method.
+ *
+ * @param pageClass the page class with the error
+ * @param exception the error causing exception
+ * @return a new ErrorPage instance
+ */
+ protected ErrorPage createErrorPage(Class extends Page> pageClass, Throwable exception) {
+ try {
+ return (ErrorPage) configService.getErrorPageClass().newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Return the application configuration service instance.
+ *
+ * @return the application configuration service instance
+ */
+ protected ConfigService getConfigService() {
+ return configService;
+ }
+
+ /**
+ * Return a new Page instance for the given path. The path must start with
+ * a "/".
+ *
+ * @param path the path which maps to a Page class
+ * @param request the Page request
+ * @return a new Page object
+ * @throws IllegalArgumentException if the Page is not found
+ */
+ @SuppressWarnings("unchecked")
+ protected T createPage(String path, HttpServletRequest request) {
+ Class extends Page> pageClass = getConfigService().getPageClass(path);
+
+ if (pageClass == null) {
+ String msg = "No Page class configured for path: " + path;
+ throw new IllegalArgumentException(msg);
+ }
+
+ return (T) initPage(path, pageClass, request);
+ }
+
+ /**
+ * Return a new Page instance for the page Class.
+ *
+ * @param pageClass the class of the Page to create
+ * @param request the Page request
+ * @return a new Page object
+ * @throws IllegalArgumentException if the Page Class is not configured
+ * with a unique path
+ */
+ @SuppressWarnings("unchecked")
+ protected T createPage(Class pageClass, HttpServletRequest request) {
+ String path = getConfigService().getPagePath(pageClass);
+
+ if (path == null) {
+ String msg =
+ "No path configured for Page class: " + pageClass.getName();
+ throw new IllegalArgumentException(msg);
+ }
+
+ return (T) initPage(path, pageClass, request);
+ }
+
+ /**
+ * Creates and returns a new PageImports instance for the specified page.
+ *
+ * @param page the page to create a new PageImports instance for
+ * @return the new PageImports instance
+ */
+ protected PageImports createPageImports(Page page) {
+ 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();
+
+ ControlRegistry controlRegistry = ControlRegistry.getThreadLocalRegistry();
+
+ // TODO Ajax requests shouldn't reach this code path since errors
+ // are rendered directly
+ if (page instanceof ErrorPage) {
+ ErrorPage errorPage = (ErrorPage) page;
+ errorPage.setMode(configService.getApplicationMode());
+
+ // Notify the dispatcher and registry of the error
+ eventDispatcher.errorOccurred(errorPage.getError());
+ controlRegistry.errorOccurred(errorPage.getError());
+ }
+
+ boolean continueProcessing = performOnSecurityCheck(page, context);
+
+ ActionResult actionResult = null;
+ if (continueProcessing) {
+
+ // Handle page method
+ String pageAction = context.getRequestParameter(Page.PAGE_ACTION);
+ if (pageAction != null) {
+ continueProcessing = false;
+
+ // Returned action result could be null
+ actionResult = performPageAction(page, pageAction, context);
+
+ controlRegistry.processPreResponse(context);
+ controlRegistry.processPreRenderHeadElements(context);
+
+ renderActionResult(actionResult, page, context);
+ }
+ }
+
+ if (continueProcessing) {
+ performOnInit(page, context);
+
+ // TODO: Ajax doesn't support forward. Is it still necessary to
+ // check isForward?
+ if (controlRegistry.hasAjaxTargetControls() && !context.isForward()) {
+
+ // Perform onProcess for regsitered Ajax target controls
+ processAjaxTargetControls(context, eventDispatcher, controlRegistry);
+
+ // Fire AjaxBehaviors registered during the onProcess event
+ // The target AjaxBehavior will set the eventDispatcher action
+ // result instance to render
+ eventDispatcher.fireAjaxBehaviors(context);
+
+ // Ensure we execute the beforeResponse and beforeGetHeadElements
+ // for Ajax requests
+ controlRegistry.processPreResponse(context);
+ controlRegistry.processPreRenderHeadElements(context);
+
+ actionResult = eventDispatcher.getActionResult();
+
+ // Render the actionResult
+ renderActionResult(actionResult, page, context);
+
+ } else {
+
+ // If no Ajax target controls have been registered fallback to
+ // the old behavior of processing and rendering the page template
+ if (logger.isTraceEnabled()) {
+ String msg = " *no* Ajax target controls have been registered."
+ + " Will process the page as a normal non Ajax request.";
+ logger.trace(msg);
+ }
+
+ continueProcessing = performOnProcess(page, context, eventDispatcher);
+
+ if (continueProcessing) {
+ performOnPostOrGet(page, context, context.isPost());
+
+ performOnRender(page, context);
+ }
+
+ // If Ajax request does not target a valid page, return a 404
+ // repsonse status, allowing JavaScript to display a proper message
+ if (ConfigService.NOT_FOUND_PATH.equals(page.getPath())) {
+ HttpServletResponse response = context.getResponse();
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ controlRegistry.processPreResponse(context);
+ controlRegistry.processPreRenderHeadElements(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 an ActionResult
+ // in the onSecurityCheck event
+ // Note: this code path is also followed if a pageAction is invoked
+ }
+ }
+
+ /**
+ * Process all Ajax target controls and return true if the page should continue
+ * processing, false otherwise.
+ *
+ * @param context the request context
+ * @param eventDispatcher the event dispatcher
+ * @param controlRegistry the control registry
+ * @return true if the page should continue processing, false otherwise
+ */
+ protected boolean processAjaxTargetControls(Context context,
+ ActionEventDispatcher eventDispatcher, ControlRegistry controlRegistry) {
+
+ boolean continueProcessing = true;
+
+ // Resolve the Ajax target control for this request
+ Control ajaxTarget = resolveAjaxTargetControl(context, controlRegistry);
+
+ if (ajaxTarget != null) {
+
+ // 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.hasAjaxBehaviorSourceSet()) {
+ logger.trace(" *no* AjaxBehavior was registered while processing the control");
+ }
+ }
+ }
+
+ return continueProcessing;
+ }
+
+ /**
+ * Provides an Ajax exception handler. Exceptions are wrapped inside a
+ * div element and streamed back to the browser. The response status
+ * is set to an {@link jakarta.servlet.http.HttpServletResponse#SC_INTERNAL_SERVER_ERROR HTTP 500 error}
+ * which allows the JavaScript that initiated the Ajax request to handle
+ * the error as appropriate.
+ *
+ * If Click is running in development modes the exception stackTrace
+ * will be rendered, in production modes an error message is
+ * rendered.
+ *
+ * Below is an example error response:
+ *
+ *
+ * <div id='errorReport' class='errorReport'>
+ * The application encountered an unexpected error.
+ * </div>
+ *
+ *
+ * @param request the servlet request
+ * @param response the servlet response
+ * @param isPost determines whether the request is a POST
+ * @param exception the error causing exception
+ * @param pageClass the page class with the error
+ */
+ 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 an ErrorReport instance instead?
+ writer.write("
");
+ } 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
+
+ /**
+ * Return the OGNL MemberAccess. This method performs a lazy load
+ * of the MemberAccess object, using a {@link DefaultMemberAccess} instance.
+ *
+ * @return the OGNL MemberAccess
+ */
+ MemberAccess getMemberAccess() {
+ if (memberAccess == null) {
+ memberAccess = new DefaultMemberAccess(true);
+ }
+ return memberAccess;
+ }
+
+ /**
+ * Create a Click application ConfigService instance.
+ *
+ * @param servletContext the Servlet Context
+ * @return a new application ConfigService instance
+ * @throws Exception if an initialization error occurs
+ */
+ @SuppressWarnings("unchecked")
+ ConfigService createConfigService(ServletContext servletContext)
+ throws Exception {
+
+ Class extends ConfigService> serviceClass = XmlConfigService.class;
+
+ String classname = servletContext.getInitParameter(CONFIG_SERVICE_CLASS);
+ if (StringUtils.isNotBlank(classname)) {
+ serviceClass = ClickUtils.classForName(classname);
+ }
+
+ return serviceClass.newInstance();
+ }
+
+ /**
+ * Initialize the Click application ConfigService instance and bind
+ * it as a ServletContext attribute using the key
+ * "org.apache.click.service.ConfigService".
+ *
+ * This method will use the configuration service class specified by the
+ * {@link #CONFIG_SERVICE_CLASS} parameter, otherwise it will create a
+ * {@link org.apache.click.service.XmlConfigService} instance.
+ *
+ * @param servletContext the servlet context to retrieve the
+ * {@link #CONFIG_SERVICE_CLASS} from
+ * @throws RuntimeException if the configuration service cannot be
+ * initialized
+ */
+ void initConfigService(ServletContext servletContext) {
+
+ if (configService != null) {
+ try {
+
+ // Note this order is very important as components need to lookup
+ // the configService out of the ServletContext while the service
+ // is being initialized.
+ servletContext.setAttribute(ConfigService.CONTEXT_NAME, configService);
+
+ // Initialize the ConfigService instance
+ configService.onInit(servletContext);
+
+ } catch (Exception e) {
+
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Destroy the application configuration service instance and remove
+ * it from the ServletContext attribute.
+ *
+ * @param servletContext the servlet context
+ * @throws RuntimeException if the configuration service cannot be
+ * destroyed
+ */
+ void destroyConfigService(ServletContext servletContext) {
+
+ if (configService != null) {
+
+ try {
+ configService.onDestroy();
+
+ } catch (Exception e) {
+
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ throw new RuntimeException(e);
+ }
+ } finally {
+ servletContext.setAttribute(ConfigService.CONTEXT_NAME, null);
+ }
+ }
+ }
+
+ /**
+ * Process all the Pages public fields using the given callback.
+ *
+ * @param page the page to obtain the fields from
+ * @param callback the fields iterator callback
+ */
+ void processPageFields(Page page, ClickServlet.FieldCallback callback) {
+
+ Field[] fields = configService.getPageFieldArray(page.getClass());
+
+ if (fields != null) {
+ for (int i = 0; i < fields.length; i++) {
+ Field field = fields[i];
+
+ try {
+ Object fieldValue = field.get(page);
+
+ if (fieldValue != null) {
+ callback.processField(field.getName(), fieldValue);
+ }
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ List getThreadLocalInterceptors() {
+ List listeners =
+ THREAD_LOCAL_INTERCEPTORS.get();
+
+ if (listeners != null) {
+ return listeners;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ void setThreadLocalInterceptors(List listeners) {
+ THREAD_LOCAL_INTERCEPTORS.set(listeners);
+ }
+
+ /**
+ * Retrieve a writer instance for the given context.
+ *
+ * @param response the servlet response
+ * @return a writer instance
+ * @throws IOException if an input or output exception occurred
+ */
+ Writer getWriter(HttpServletResponse response) throws IOException {
+ try {
+
+ return response.getWriter();
+
+ } catch (IllegalStateException ignore) {
+ // If writer cannot be retrieved fallback to OutputStream. CLK-644
+ 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
+
+ /**
+ * Field iterator callback.
+ */
+ static interface FieldCallback {
+
+ /**
+ * Callback method invoked for each field.
+ *
+ * @param fieldName the field name
+ * @param fieldValue the field value
+ */
+ public void processField(String fieldName, Object fieldValue);
+
+ }
+
+ // Private methods --------------------------------------------------------
+
+ /**
+ * Resolve and return the Ajax target control for this request or null if no
+ * Ajax target was found.
+ *
+ * @param context the request context
+ * @param controlRegistry the control registry
+ * @return the target Ajax target control or null if no Ajax target was found
+ */
+ private Control resolveAjaxTargetControl(Context context, ControlRegistry controlRegistry) {
+
+ Control ajaxTarget = null;
+
+ if (logger.isTraceEnabled()) {
+ logger.trace(" the following controls have been registered as potential Ajax targets:");
+ if (controlRegistry.hasAjaxTargetControls()) {
+ for (Control control : controlRegistry.getAjaxTargetControls()) {
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+ String controlClassName = ClassUtils.getShortClassName(control.getClass());
+ buffer.append(" ").append(controlClassName);
+ buffer.append(": name='").append(control.getName()).append("'");
+ logger.trace(buffer.toString());
+ }
+ } else {
+ logger.trace(" *no* control has been registered");
+ }
+ }
+
+ for (Control control : controlRegistry.getAjaxTargetControls()) {
+
+ if (control.isAjaxTarget(context)) {
+ ajaxTarget = control;
+ // The first matching control will be processed. Multiple matching
+ // controls are not supported
+ break;
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+ if (ajaxTarget == null) {
+ String msg = " *no* target control was found for the Ajax request";
+ logger.trace(msg);
+
+ } else {
+ 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 target control found)");
+ logger.trace(buffer.toString());
+ }
+ }
+
+ return ajaxTarget;
+ }
+
+ /**
+ * Log the request parameter names and values.
+ *
+ * @param request the HTTP servlet request
+ */
+ private void logRequestParameters(HttpServletRequest request) {
+
+ Map requestParams = new TreeMap();
+
+ Enumeration> e = request.getParameterNames();
+ while (e.hasMoreElements()) {
+ String name = e.nextElement().toString();
+ String[] values = request.getParameterValues(name);
+ requestParams.put(name, values);
+ }
+
+ for (Map.Entry entry : requestParams.entrySet()) {
+ String name = entry.getKey();
+ String[] values = entry.getValue();
+
+ HtmlStringBuffer buffer = new HtmlStringBuffer(40);
+ buffer.append(" request param: " + name + "=");
+
+ if (values == null) {
+ // ignore
+ } else if (values.length == 1) {
+
+ buffer.append(ClickUtils.limitLength(values[0], 40));
+ } else {
+
+ for (int i = 0; i < values.length; i++) {
+ if (i == 0) {
+ buffer.append('[');
+ } else {
+ buffer.append(", ");
+ }
+ buffer.append(ClickUtils.limitLength(values[i], 40));
+ }
+ buffer.append("]");
+ }
+
+ logger.trace(buffer.toString());
+ }
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/Context.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/Context.java
new file mode 100644
index 0000000000..ac6ddbe9b4
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/Context.java
@@ -0,0 +1,973 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Map;
+
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+import org.openidentityplatform.openam.click.service.FileUploadService;
+import org.openidentityplatform.openam.click.service.LogService;
+import org.openidentityplatform.openam.click.service.MessagesMapService;
+import org.openidentityplatform.openam.click.service.TemplateService;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.apache.click.util.FlashAttribute;
+import org.apache.commons.fileupload.FileItem;
+
+/**
+ * Provides the HTTP request context information for pages and controls.
+ * A new Context object is created for each Page request. The request Context
+ * object can be obtained from the thread local variable via the
+ * {@link org.openidentityplatform.openam.click.Context#getThreadLocalContext()} method.
+ */
+public class Context {
+
+ // -------------------------------------------------------------- Constants
+
+ /** The user's session Locale key: locale. */
+ public static final String LOCALE = "locale";
+
+ /**
+ * The attribute key used for storing any error that occurred while
+ * Context is created.
+ */
+ static final String CONTEXT_FATAL_ERROR = "_context_fatal_error";
+
+ /** The thread local context stack. */
+ private static final ThreadLocal THREAD_LOCAL_CONTEXT_STACK = new ThreadLocal<>();
+
+ // ----------------------------------------------------- Instance Variables
+
+ /** The servlet context. */
+ protected final ServletContext context;
+
+ /** The servlet config. */
+ protected final ServletConfig config;
+
+ /** The HTTP method is POST flag. */
+ protected final boolean isPost;
+
+ /** The servlet response. */
+ protected final HttpServletResponse response;
+
+ /** The click services interface. */
+ final ClickServlet clickServlet;
+
+ /** The servlet request. */
+ final HttpServletRequest request;
+
+ // ----------------------------------------------------------- Constructors
+
+ /**
+ * Create a new request context.
+ *
+ * @param context the servlet context
+ * @param config the servlet config
+ * @param request the servlet request
+ * @param response the servlet response
+ * @param isPost the servlet request is a POST
+ * @param clickServlet the click servlet instance
+ */
+ public Context(ServletContext context, ServletConfig config,
+ HttpServletRequest request, HttpServletResponse response,
+ boolean isPost, ClickServlet clickServlet) {
+
+ this.clickServlet = clickServlet;
+ this.context = context;
+ this.config = config;
+ this.isPost = isPost;
+
+ Context.ContextStack contextStack = getContextStack();
+
+ if (contextStack.size() == 0) {
+
+ // CLK-312. Apply request.setCharacterEncoding before wrapping
+ // request in ClickRequestWrapper
+ String charset = clickServlet.getConfigService().getCharset();
+ if (charset != null) {
+
+ try {
+ request.setCharacterEncoding(charset);
+
+ } catch (UnsupportedEncodingException ex) {
+ String msg =
+ "The character encoding " + charset + " is invalid.";
+ LogService logService =
+ clickServlet.getConfigService().getLogService();
+ logService.warn(msg, ex);
+ }
+ }
+
+ FileUploadService fus =
+ clickServlet.getConfigService().getFileUploadService();
+
+ this.request = new ClickRequestWrapper(request, fus);
+
+ } else {
+ this.request = request;
+ }
+ this.response = response;
+ }
+
+ /**
+ * Package private constructor to support Mock objects.
+ *
+ * @param request the servlet request
+ * @param response the servlet response
+ */
+ Context(HttpServletRequest request, HttpServletResponse response) {
+ // This method should be removed once the deprecated MockContext
+ // constructor is removed
+ this.request = request;
+ this.response = response;
+ this.clickServlet = null;
+ this.context = null;
+ this.config = null;
+ this.isPost = "POST".equalsIgnoreCase(request.getMethod());
+ }
+
+ // --------------------------------------------------------- Public Methods
+
+ /**
+ * Return the thread local request context instance.
+ *
+ * @return the thread local request context instance.
+ * @throws RuntimeException if a Context is not available on the thread.
+ */
+ public static Context getThreadLocalContext() {
+ return getContextStack().peek();
+ }
+
+ /**
+ * Returns true if a Context instance is available on the current thread,
+ * false otherwise. Unlike {@link #getThreadLocalContext()} this method
+ * can safely be used and will not throw an exception if Context is not
+ * available on the current thread.
+ *
+ * This method is very useful inside a {@link Control} constructor which
+ * might need access to the Context. As Controls could potentially be
+ * instantiated during Click startup (in order to deploy their resources),
+ * this check can be used to determine whether Context is available or not.
+ *
+ * For example:
+ *
+ *
+ * public MyControl extends AbstractControl {
+ * public MyControl(String name) {
+ * if (Context.hasThreadLocalContext()) {
+ * // Context is available, meaning a user initiated a web
+ * // request
+ * Context context = getContext();
+ * String state = (String) context.getSessionAttribute(name);
+ * setValue(state);
+ * } else {
+ * // No Context is available, meaning this is most probably
+ * // the application startup and deployment phase.
+ * }
+ * }
+ * }
+ *
+ *
+ * @return true if a Context instance is available on the current thread,
+ * false otherwise
+ */
+ public static boolean hasThreadLocalContext() {
+ Context.ContextStack contextStack = THREAD_LOCAL_CONTEXT_STACK.get();
+ if (contextStack == null) {
+ return false;
+ }
+ return !contextStack.isEmpty();
+ }
+
+ /**
+ * Returns the servlet request.
+ *
+ * @return HttpServletRequest
+ */
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+
+ /**
+ * Returns the servlet response.
+ *
+ * @return HttpServletResponse
+ */
+ public HttpServletResponse getResponse() {
+ return response;
+ }
+
+ /**
+ * Returns the servlet config.
+ *
+ * @return ServletConfig
+ */
+ public ServletConfig getServletConfig() {
+ return config;
+ }
+
+ /**
+ * Returns the servlet context.
+ *
+ * @return ServletContext
+ */
+ public ServletContext getServletContext() {
+ return context;
+ }
+
+ /**
+ * Return the user's HttpSession, creating one if necessary.
+ *
+ * @return the user's HttpSession, creating one if necessary.
+ */
+ public HttpSession getSession() {
+ return request.getSession();
+ }
+
+ /**
+ * Return the page resource path from the request. For example:
+ *
+ *
+ * @return the page resource path from the request
+ */
+ public String getResourcePath() {
+ return ClickUtils.getResourcePath(request);
+ }
+
+ /**
+ * Return true if the request has been forwarded. A forwarded request
+ * will contain a {@link ClickServlet#CLICK_FORWARD} request attribute.
+ *
+ * @return true if the request has been forwarded
+ */
+ public boolean isForward() {
+ return (request.getAttribute(ClickServlet.CLICK_FORWARD) != null);
+ }
+
+ /**
+ * Return true if the HTTP request method is "POST".
+ *
+ * @return true if the HTTP request method is "POST"
+ */
+ public boolean isPost() {
+ return isPost;
+ }
+
+ /**
+ * Return true if the HTTP request method is "GET".
+ *
+ * @return true if the HTTP request method is "GET"
+ */
+ public boolean isGet() {
+ return !isPost && getRequest().getMethod().equalsIgnoreCase("GET");
+ }
+
+ /**
+ * Return true is this is an Ajax request, false otherwise.
+ *
+ * An Ajax request is identified by the presence of the request header
+ * or request parameter: "X-Requested-With".
+ * "X-Requested-With" is the de-facto standard identifier used by
+ * Ajax libraries.
+ *
+ * Note: incoming requests that contains a request parameter
+ * "X-Requested-With" will result in this method returning true, even
+ * though the request itself was not initiated through a XmlHttpRequest
+ * object. This allows one to programmatically enable Ajax requests. A common
+ * use case for this feature is when uploading files through an IFrame element.
+ * By specifying "X-Requested-With" as a request parameter the IFrame
+ * request will be handled like a normal Ajax request.
+ *
+ * @return true if this is an Ajax request, false otherwise
+ */
+ public boolean isAjaxRequest() {
+ return ClickUtils.isAjaxRequest(getRequest());
+ }
+
+ /**
+ * Return true if the request contains the named attribute.
+ *
+ * @param name the name of the request attribute
+ * @return true if the request contains the named attribute
+ */
+ public boolean hasRequestAttribute(String name) {
+ return (getRequestAttribute(name) != null);
+ }
+
+ /**
+ * Return the named request attribute, or null if not defined.
+ *
+ * @param name the name of the request attribute
+ * @return the named request attribute, or null if not defined
+ */
+ public Object getRequestAttribute(String name) {
+ return request.getAttribute(name);
+ }
+
+ /**
+ * This method will set the named object in the HTTP request.
+ *
+ * @param name the storage name for the object in the request
+ * @param value the object to store in the request
+ */
+ public void setRequestAttribute(String name, Object value) {
+ request.setAttribute(name, value);
+ }
+
+ /**
+ * Return true if the request contains the named parameter.
+ *
+ * @param name the name of the request parameter
+ * @return true if the request contains the named parameter
+ */
+ public boolean hasRequestParameter(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("hasRequestParameter was called"
+ + " with null name argument. This is often caused when a"
+ + " Control binds to a request parameter, but its name was not"
+ + " set.");
+ }
+ return (getRequestParameter(name) != null);
+ }
+
+ /**
+ * Return the named request parameter. This method supports
+ * "multipart/form-data" POST requests and should be used in
+ * preference to the HttpServletRequest method
+ * getParameter().
+ *
+ * @see org.apache.click.control.Form#onProcess()
+ * @see #isMultipartRequest()
+ * @see #getFileItemMap()
+ *
+ * @param name the name of the request parameter
+ * @return the value of the request parameter.
+ */
+ public String getRequestParameter(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("getRequestParameter was called"
+ + " with null name argument. This is often caused when a"
+ + " Control binds to a request parameter, but its name was not"
+ + " set.");
+ }
+ return request.getParameter(name);
+ }
+
+ /**
+ * Returns an array of String objects containing all of the values the given
+ * request parameter has, or null if the parameter does not exist.
+ *
+ * @param name a String containing the name of the parameter whose
+ * value is requested
+ * @return an array of String objects containing the parameter's values
+ */
+ public String[] getRequestParameterValues(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("getRequestParameter was called"
+ + " with null name argument. This is often caused when a"
+ + " Control binds to a request parameter, but its name was not"
+ + " set.");
+ }
+ return request.getParameterValues(name);
+ }
+
+ /**
+ * Return the named session attribute, or null if not defined.
+ *
+ * If the session is not defined this method will return null, and a
+ * session will not be created.
+ *
+ * This method supports {@link FlashAttribute} which when accessed are then
+ * removed from the session.
+ *
+ * @param name the name of the session attribute
+ * @return the named session attribute, or null if not defined
+ */
+ public Object getSessionAttribute(String name) {
+ if (hasSession()) {
+ Object object = getSession().getAttribute(name);
+
+ if (object instanceof FlashAttribute) {
+ FlashAttribute flashObject = (FlashAttribute) object;
+ object = flashObject.getValue();
+ removeSessionAttribute(name);
+ }
+
+ return object;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * This method will set the named object in the HttpSession.
+ *
+ * This method will create a session if one does not already exist.
+ *
+ * @param name the storage name for the object in the session
+ * @param value the object to store in the session
+ */
+ public void setSessionAttribute(String name, Object value) {
+ getSession().setAttribute(name, value);
+ }
+
+ /**
+ * Remove the named attribute from the session. If the session does not
+ * exist or the name is null, this method does nothing.
+ *
+ * @param name of the attribute to remove from the session
+ */
+ public void removeSessionAttribute(String name) {
+ if (hasSession() && name != null) {
+ getSession().removeAttribute(name);
+ }
+ }
+
+ /**
+ * Return true if there is a session and it contains the named attribute.
+ *
+ * @param name the name of the attribute
+ * @return true if the session contains the named attribute
+ */
+ public boolean hasSessionAttribute(String name) {
+ return (getSessionAttribute(name) != null);
+ }
+
+ /**
+ * Return true if a HttpSession exists, or false otherwise.
+ *
+ * @return true if a HttpSession exists, or false otherwise
+ */
+ public boolean hasSession() {
+ return (request.getSession(false) != null);
+ }
+
+ /**
+ * This method will set the named object as a flash HttpSession object.
+ *
+ * The flash object will exist in the session until it is accessed once,
+ * and then removed. Flash objects are typically used to display a message
+ * once.
+ *
+ * @param name the storage name for the object in the session
+ * @param value the object to store in the session
+ */
+ public void setFlashAttribute(String name, Object value) {
+ getSession().setAttribute(name, new FlashAttribute(value));
+ }
+
+ /**
+ * Return the cookie for the given name or null if not found.
+ *
+ * @param name the name of the cookie
+ * @return the cookie for the given name or null if not found
+ */
+ public Cookie getCookie(String name) {
+ return ClickUtils.getCookie(getRequest(), name);
+ }
+
+ /**
+ * Return the cookie value for the given name or null if not found.
+ *
+ * @param name the name of the cookie
+ * @return the cookie value for the given name or null if not found
+ */
+ public String getCookieValue(String name) {
+ return ClickUtils.getCookieValue(getRequest(), name);
+ }
+
+ /**
+ * Sets the given cookie value in the servlet response with the path "/".
+ *
+ * @see ClickUtils#setCookie(HttpServletRequest, HttpServletResponse, String, String, int, String)
+ *
+ * @param name the cookie name
+ * @param value the cookie value
+ * @param maxAge the maximum age of the cookie in seconds. A negative
+ * value will expire the cookie at the end of the session, while 0 will delete
+ * the cookie.
+ * @return the Cookie object created and set in the response
+ */
+ public Cookie setCookie(String name, String value, int maxAge) {
+ return ClickUtils.setCookie(getRequest(),
+ getResponse(),
+ name,
+ value,
+ maxAge,
+ "/");
+ }
+
+ /**
+ * Invalidate the specified cookie and delete it from the response object.
+ * Deletes only cookies mapped against the root "/" path.
+ *
+ * @see ClickUtils#invalidateCookie(HttpServletRequest, HttpServletResponse, String)
+ *
+ * @param name the name of the cookie you want to delete.
+ */
+ public void invalidateCookie(String name) {
+ ClickUtils.invalidateCookie(getRequest(), getResponse(), name);
+ }
+
+ /**
+ * Return a new Page instance for the given path.
+ *
+ * This method can be used to create a target page for the
+ * {@link org.apache.click.Page#setForward(org.apache.click.Page)}, for example:
+ *
+ *
+ *
+ * The given page path must start with a '/'.
+ *
+ * @param path the Page path as configured in the click.xml file
+ * @return a new Page object
+ * @throws IllegalArgumentException if the Page is not found
+ */
+ @SuppressWarnings("unchecked")
+ public T createPage(String path) {
+ if (path == null || path.length() == 0) {
+ throw new IllegalArgumentException("page path cannot be null or empty");
+ }
+
+ if (path.charAt(0) != '/') {
+ throw new IllegalArgumentException("page path must start with a '/'");
+ }
+ return (T) clickServlet.createPage(path, request);
+ }
+
+ /**
+ * Return a new Page instance for the given class.
+ *
+ * This method can be used to create a target page for the
+ * {@link org.apache.click.Page#setForward(org.apache.click.Page)}, for example:
+ *
+ *
+ *
+ * @param pageClass the Page class as configured in the click.xml file
+ * @return a new Page object
+ * @throws IllegalArgumentException if the Page is not found, or is not
+ * configured with a unique path
+ */
+ public T createPage(Class pageClass) {
+ return clickServlet.createPage(pageClass, request);
+ }
+
+ /**
+ * Return the path for the given page Class.
+ *
+ * @param pageClass the class of the Page to lookup the path for
+ * @return the path for the given page Class
+ * @throws IllegalArgumentException if the Page Class is not configured
+ * with a unique path
+ */
+ public String getPagePath(Class extends Page> pageClass) {
+ return clickServlet.getConfigService().getPagePath(pageClass);
+ }
+
+ /**
+ * Return the page Class for the given path.
+ *
+ * @param path the page path
+ * @return the page class for the given path
+ * @throws IllegalArgumentException if the Page Class for the path is not
+ * found
+ */
+ public Class extends Page> getPageClass(String path) {
+ return clickServlet.getConfigService().getPageClass(path);
+ }
+
+ /**
+ * Return the Click application mode value:
+ * ["production", "profile", "development", "debug", "trace"].
+ *
+ * @return the application mode value
+ */
+ public String getApplicationMode() {
+ return clickServlet.getConfigService().getApplicationMode();
+ }
+
+ /**
+ * Return the Click application charset or ISO-8859-1 if not is defined.
+ *
+ * The charset is defined in click.xml through the charset attribute
+ * on the click-app element.
+ *
+ *
+ *
+ * @return the application charset or ISO-8859-1 if not defined
+ */
+ public String getCharset() {
+ String charset = clickServlet.getConfigService().getCharset();
+ if (charset == null) {
+ charset = "ISO-8859-1";
+ }
+ return charset;
+ }
+
+ /**
+ * Return a new messages map for the given baseClass (a page or control)
+ * and the given global resource bundle name.
+ *
+ * @param baseClass the target class
+ * @param globalResource the global resource bundle name
+ * @return a new messages map with the messages for the target.
+ */
+ public Map createMessagesMap(Class> baseClass, String globalResource) {
+ MessagesMapService messagesMapService =
+ clickServlet.getConfigService().getMessagesMapService();
+
+ return messagesMapService.createMessagesMap(baseClass, globalResource, getLocale());
+ }
+
+ /**
+ * Returns a map of FileItem arrays keyed on request parameter
+ * name for "multipart" POST requests (file uploads). Thus each map entry
+ * will consist of one or more FileItem objects.
+ *
+ * @return map of FileItem arrays keyed on request parameter name
+ * for "multipart" POST requests
+ */
+ public Map getFileItemMap() {
+ return findClickRequestWrapper(request).getFileItemMap();
+ }
+
+ /**
+ * Returns the value of a request parameter as a FileItem, for
+ * "multipart" POST requests (file uploads), or null if the parameter
+ * is not found.
+ *
+ * If there were multivalued parameters in the request (ie two or more
+ * file upload fields with the same name), the first fileItem
+ * in the array is returned.
+ *
+ * @param name the name of the parameter of the fileItem to retrieve
+ *
+ * @return the fileItem for the specified name
+ */
+ public FileItem getFileItem(String name) {
+ Object value = findClickRequestWrapper(request).getFileItemMap().get(name);
+
+ if (value != null) {
+ if (value instanceof FileItem[]) {
+ FileItem[] array = (FileItem[]) value;
+ if (array.length >= 1) {
+ return array[0];
+ }
+ } else if (value instanceof FileItem) {
+ return (FileItem) value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the users Locale.
+ *
+ * If the users Locale is stored in their session this will be returned.
+ * Else if the click-app configuration defines a default Locale this
+ * value will be returned, otherwise the request's Locale will be returned.
+ *
+ * To override the default request Locale set the users Locale using the
+ * {@link #setLocale(Locale)} method.
+ *
+ * Pages and Controls obtain the users Locale using this method.
+ *
+ * @return the users Locale in the session, or if null the request Locale
+ */
+ public Locale getLocale() {
+ Locale locale = (Locale) getSessionAttribute(LOCALE);
+
+ if (locale == null) {
+
+ if (clickServlet.getConfigService().getLocale() != null) {
+ locale = clickServlet.getConfigService().getLocale();
+
+ } else {
+ locale = getRequest().getLocale();
+ }
+ }
+
+ return locale;
+ }
+
+ /**
+ * This method stores the given Locale in the users session. If the given
+ * Locale is null, the "locale" attribute will be removed from the session.
+ *
+ * The Locale object is stored in the session using the {@link #LOCALE}
+ * key.
+ *
+ * @param locale the Locale to store in the users session using the key
+ * "locale"
+ */
+ public void setLocale(Locale locale) {
+ if (locale == null && hasSession()) {
+ getSession().removeAttribute(LOCALE);
+ } else {
+ setSessionAttribute(LOCALE, locale);
+ }
+ }
+
+ /**
+ * Return true if the request is a multi-part content type POST request.
+ *
+ * @return true if the request is a multi-part content type POST request
+ */
+ public boolean isMultipartRequest() {
+ return ClickUtils.isMultipartRequest(request);
+ }
+
+ /**
+ * Return a rendered Velocity template and model for the given
+ * class and model data.
+ *
+ * This method will merge the class .htm Velocity template and
+ * model data using the applications Velocity Engine.
+ *
+ * An example of the class template resolution is provided below:
+ *
+ * // Full class name
+ * com.mycorp.control.CustomTextField
+ *
+ * // Template path name
+ * /com/mycorp/control/CustomTextField.htm
+ *
+ * Example method usage:
+ *
+ * public String toString() {
+ * Map model = getModel();
+ * return getContext().renderTemplate(getClass(), model);
+ * }
+ *
+ * @param templateClass the class to resolve the template for
+ * @param model the model data to merge with the template
+ * @return rendered Velocity template merged with the model data
+ * @throws RuntimeException if an error occurs
+ */
+ @SuppressWarnings("unchecked")
+ public String renderTemplate(Class templateClass, Map model) {
+
+ if (templateClass == null) {
+ String msg = "Null templateClass parameter";
+ throw new IllegalArgumentException(msg);
+ }
+
+ String templatePath = templateClass.getName();
+ templatePath = '/' + templatePath.replace('.', '/') + ".htm";
+
+ return renderTemplate(templatePath, model);
+ }
+
+ /**
+ * Return a rendered Velocity template and model data.
+ *
+ * Example method usage:
+ *
+ * public String toString() {
+ * Map model = getModel();
+ * return getContext().renderTemplate("/custom-table.htm", model);
+ * }
+ *
+ * @param templatePath the path of the Velocity template to render
+ * @param model the model data to merge with the template
+ * @return rendered Velocity template merged with the model data
+ * @throws RuntimeException if an error occurs
+ */
+ public String renderTemplate(String templatePath, Map model) {
+
+ if (templatePath == null) {
+ String msg = "Null templatePath parameter";
+ throw new IllegalArgumentException(msg);
+ }
+
+ if (model == null) {
+ String msg = "Null model parameter";
+ throw new IllegalArgumentException(msg);
+ }
+
+ StringWriter stringWriter = new StringWriter(1024);
+
+ TemplateService templateService =
+ clickServlet.getConfigService().getTemplateService();
+
+ try {
+ templateService.renderTemplate(templatePath, model, stringWriter);
+
+ } catch (Exception e) {
+ String msg = "Error occurred rendering template: "
+ + templatePath + "\n";
+ clickServlet.getConfigService().getLogService().error(msg, e);
+
+ throw new RuntimeException(e);
+ }
+
+ return stringWriter.toString();
+ }
+
+ // ------------------------------------------------ Package Private Methods
+
+ /**
+ * Return the stack data structure where Context's are stored.
+ *
+ * @return stack data structure where Context's are stored
+ */
+ static Context.ContextStack getContextStack() {
+ Context.ContextStack contextStack = THREAD_LOCAL_CONTEXT_STACK.get();
+
+ if (contextStack == null) {
+ contextStack = new Context.ContextStack(2);
+ THREAD_LOCAL_CONTEXT_STACK.set(contextStack);
+ }
+
+ return contextStack;
+ }
+
+ /**
+ * Adds the specified Context on top of the Context stack.
+ *
+ * @param context a context instance
+ */
+ static void pushThreadLocalContext(Context context) {
+ getContextStack().push(context);
+ }
+
+ /**
+ * Remove and return the context instance on top of the context stack.
+ *
+ * @return the context instance on top of the context stack
+ */
+ static Context popThreadLocalContext() {
+ Context.ContextStack contextStack = getContextStack();
+ Context context = contextStack.pop();
+
+ if (contextStack.isEmpty()) {
+ THREAD_LOCAL_CONTEXT_STACK.remove();
+ }
+
+ return context;
+ }
+
+ /**
+ * Find and return the ClickRequestWrapper that is wrapped by the specified
+ * request.
+ *
+ * @param request the servlet request that wraps the ClickRequestWrapper
+ * @return the wrapped servlet request
+ */
+ static ClickRequestWrapper findClickRequestWrapper(ServletRequest request) {
+
+ while (!(request instanceof ClickRequestWrapper)
+ && request instanceof HttpServletRequestWrapper
+ && request != null) {
+ request = ((HttpServletRequestWrapper) request).getRequest();
+ }
+
+ if (request instanceof ClickRequestWrapper) {
+ return (ClickRequestWrapper) request;
+ } else {
+ throw new IllegalStateException("Click request is not present");
+ }
+ }
+
+ // -------------------------------------------------- Inner Classes
+
+ /**
+ * Provides an unsynchronized Context Stack.
+ */
+ static final class ContextStack extends ArrayList {
+
+ /** Serialization version indicator. */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new ContextStack with the given initial capacity.
+ *
+ * @param initialCapacity specify initial capacity of this stack
+ */
+ private ContextStack(int initialCapacity) {
+ super(initialCapacity);
+ }
+
+ /**
+ * Pushes the Context onto the top of this stack.
+ *
+ * @param context the Context to push onto this stack
+ * @return the Context pushed on this stack
+ */
+ private Context push(Context context) {
+ add(context);
+
+ return context;
+ }
+
+ /**
+ * Removes and return the Context at the top of this stack.
+ *
+ * @return the Context at the top of this stack
+ */
+ private Context pop() {
+ Context context = peek();
+
+ remove(size() - 1);
+
+ return context;
+ }
+
+ /**
+ * Looks at the Context at the top of this stack without removing it.
+ *
+ * @return the Context at the top of this stack
+ */
+ private Context peek() {
+ int length = size();
+
+ if (length == 0) {
+ String msg = "No Context available on ThreadLocal Context Stack";
+ throw new RuntimeException(msg);
+ }
+
+ return get(length - 1);
+ }
+ }
+
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/Control.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/Control.java
new file mode 100644
index 0000000000..96d0c9d548
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/Control.java
@@ -0,0 +1,496 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.servlet.ServletContext;
+
+import org.openidentityplatform.openam.click.element.Element;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+
+/**
+ * Provides the interface for Page controls. Controls are also referred to
+ * as components or widgets.
+ *
+ * When a Page request event is processed Controls may perform server side event
+ * processing through their {@link #onProcess()} method. Controls are generally
+ * rendered in a Page by calling their toString() method.
+ *
+ * The Control execution sequence is illustrated below:
+ *
+ *
+ *
+ *
HTML HEAD Elements
+ *
+ * Control HTML HEAD elements can be included in the Page by overriding the
+ * {@link #getHeadElements()} method.
+ *
+ * Below is an example of a custom TextField control specifying that the
+ * custom.js file should be included in the HTML HEADer:
+ *
+ *
+ * public class CustomField extends TextField {
+ *
+ * public List getHeadElements() {
+ * if(headElements == null) {
+ * // If headElements is null, create default headElements
+ * headElements = super.getHeadElements();
+ *
+ * // Add a new JavaScript Import Element for the "/custom.js" script
+ * headElements.add(new JsImport("/click/custom.js"));
+ * }
+ * return headElements;
+ * }
+ *
+ * ..
+ * }
+ *
+ *
+ *
Deploying Resources
+ *
+ * The Click framework uses the Velocity Tools WebappResourceLoader for loading templates.
+ * This avoids issues associate with using the Velocity ClasspathResourceLoader and
+ * FileResourceLoader on J2EE application servers.
+ * To make preconfigured resources (templates, JavaScript, stylesheets, etc.)
+ * available to web applications Click automatically deploys configured classpath
+ * resources to the /click directory at startup
+ * (existing files will not be overwritten).
+ *
+ * Click supports two ways of deploying pre-configured resources. The recommended
+ * deployment strategy (which also the simplest) relies on packaging resources
+ * into a special folder of the JAR, called 'META-INF/resources'. At
+ * startup time Click will scan this folder for resources and deploy them to the
+ * web application. This deployment strategy is the same approach taken by the
+ * Servlet 3.0 specification. Please see the section
+ * Deploying Custom Resources
+ * for more details.
+ *
+ * An alternative approach to deploying static resources on startup is provided
+ * by the Control interface through the {@link #onDeploy(ServletContext)} method.
+ *
+ * Continuing our example, the CustomField control deploys its
+ * custom.js file to the /click directory:
+ *
+ *
+ *
+ * Controls using the onDeploy() method must be registered in the
+ * application WEB-INF/click.xml for them to be invoked.
+ * For example:
+ *
+ *
+ *
+ * When the Click application starts up it will deploy any control elements
+ * defined in the following files in sequential order:
+ *
+ *
/click-controls.xml
+ *
/extras-controls.xml
+ *
WEB-INF/click.xml
+ *
+ *
+ * Please note {@link org.apache.click.control.AbstractControl} provides
+ * a default implementation of the Control interface to make it easier for
+ * developers to create their own controls.
+ *
+ * @see org.apache.click.util.PageImports
+ */
+public interface Control extends Serializable {
+
+ /**
+ * The global control messages bundle name: click-control.
+ */
+ public static final String CONTROL_MESSAGES = "click-control";
+
+ /**
+ * Return the Page request Context of the Control.
+ *
+ * @deprecated getContext() is now obsolete on the Control interface,
+ * but will still be available on AbstractControl:
+ * {@link org.apache.click.control.AbstractControl#getContext()}
+ *
+ * @return the Page request Context
+ */
+ public Context getContext();
+
+ /**
+ * Return the list of HEAD {@link org.apache.click.element.Element elements}
+ * to be included in the page. Example HEAD elements include
+ * {@link org.apache.click.element.JsImport JsImport},
+ * {@link org.apache.click.element.JsScript JsScript},
+ * {@link org.apache.click.element.CssImport CssImport} and
+ * {@link org.apache.click.element.CssStyle CssStyle}.
+ *
+ * Controls can contribute their own list of HEAD elements by implementing
+ * this method.
+ *
+ * The recommended approach when implementing this method is to use
+ * lazy loading to ensure the HEAD elements are only added
+ * once and when needed. For example:
+ *
+ *
+ * public MyControl extends AbstractControl {
+ *
+ * public List getHeadElements() {
+ * // Use lazy loading to ensure the JS is only added the
+ * // first time this method is called.
+ * if (headElements == null) {
+ * // Get the head elements from the super implementation
+ * headElements = super.getHeadElements();
+ *
+ * // Include the control's external JavaScript resource
+ * JsImport jsImport = new JsImport("/mycorp/mycontrol/mycontrol.js");
+ * headElements.add(jsImport);
+ *
+ * // Include the control's external Css resource
+ * CssImport cssImport = new CssImport("/mycorp/mycontrol/mycontrol.css");
+ * headElements.add(cssImport);
+ * }
+ * return headElements;
+ * }
+ * }
+ *
+ * Alternatively one can add the HEAD elements in the Control's constructor:
+ *
+ *
+ *
+ * One can also add HEAD elements from event handler methods such as
+ * {@link #onInit()}, {@link #onProcess()}, {@link #onRender()}
+ * etc.
+ *
+ * The order in which JS and CSS files are included will be preserved in the
+ * page.
+ *
+ * Note: this method must never return null. If no HEAD elements
+ * are available this method must return an empty {@link java.util.List}.
+ *
+ * Also note: a common problem when overriding getHeadElements in
+ * subclasses is forgetting to call super.getHeadElements. Consider
+ * carefully whether you should call super.getHeadElements or not.
+ *
+ * @return the list of HEAD elements to be included in the page
+ */
+ public List getHeadElements();
+
+ /**
+ * Return HTML element identifier attribute "id" value.
+ *
+ * {@link org.apache.click.control.AbstractControl#getId()}
+ *
+ * @return HTML element identifier attribute "id" value
+ */
+ public String getId();
+
+ /**
+ * Set the controls event listener.
+ *
+ * The method signature of the listener is:
+ *
must have a valid Java method name
+ *
takes no arguments
+ *
returns a boolean value
+ *
+ *
+ * An example event listener method would be:
+ *
+ *
+ *
+ * @param listener the listener object with the named method to invoke
+ * @param method the name of the method to invoke
+ *
+ * @deprecated this method is now obsolete on the Control interface, but
+ * will still be available on AbstractControl:
+ * {@link org.apache.click.control.AbstractControl#setListener(java.lang.Object, java.lang.String)}
+ */
+ public void setListener(Object listener, String method);
+
+ /**
+ * Return the localized messages Map of the Control.
+ *
+ * @return the localized messages Map of the Control
+ */
+ public Map getMessages();
+
+ /**
+ * Return the name of the Control. Each control name must be unique in the
+ * containing Page model or the containing Form.
+ *
+ * @return the name of the control
+ */
+ public String getName();
+
+ /**
+ * Set the name of the Control. Each control name must be unique in the
+ * containing Page model or the parent container.
+ *
+ * Please note: changing the name of a Control after it has been
+ * added to its parent container is undefined. Thus it is best not
+ * to change the name of a Control once its been set.
+ *
+ * @param name of the control
+ * @throws IllegalArgumentException if the name is null
+ */
+ public void setName(String name);
+
+ /**
+ * Return the parent of the Control.
+ *
+ * @return the parent of the Control
+ */
+ public Object getParent();
+
+ /**
+ * Set the parent of the Control.
+ *
+ * @param parent the parent of the Control
+ */
+ public void setParent(Object parent);
+
+ /**
+ * The on deploy event handler, which provides classes the
+ * opportunity to deploy static resources when the Click application is
+ * initialized.
+ *
+ * For example:
+ *
+ * Please note: a common problem when overriding onDeploy in
+ * subclasses is forgetting to call super.onDeploy. Consider
+ * carefully whether you should call super.onDeploy or not.
+ *
+ * Click also supports an alternative deployment strategy which relies on
+ * packaging resource (stylesheets, JavaScript, images etc.) following a
+ * specific convention. See the section
+ * Deploying Custom Resources
+ * for further details.
+ *
+ * @param servletContext the servlet context
+ */
+ public void onDeploy(ServletContext servletContext);
+
+ /**
+ * The on initialize event handler. Each control will be initialized
+ * before its {@link #onProcess()} method is called.
+ *
+ * {@link org.apache.click.control.Container} implementations should recursively
+ * invoke the onInit method on each of their child controls ensuring that
+ * all controls receive this event.
+ *
+ * Please note: a common problem when overriding onInit in
+ * subclasses is forgetting to call super.onInit(). Consider
+ * carefully whether you should call super.onInit() or not,
+ * especially for {@link org.apache.click.control.Container}s which by default
+ * call onInit on all their child controls as well.
+ */
+ public void onInit();
+
+ /**
+ * The on process event handler. Each control will be processed when the
+ * Page is requested.
+ *
+ * ClickServlet will process all Page controls in the order they were added
+ * to the Page.
+ *
+ * {@link org.apache.click.control.Container} implementations should recursively
+ * invoke the onProcess method on each of their child controls ensuring that
+ * all controls receive this event. However when a control onProcess method
+ * return false, no other controls onProcess method should be invoked.
+ *
+ * When a control is processed it should return true if the Page should
+ * continue event processing, or false if no other controls should be
+ * processed and the {@link org.apache.click.Page#onGet()} or {@link Page#onPost()} methods
+ * should not be invoked.
+ *
+ * Please note: a common problem when overriding onProcess in
+ * subclasses is forgetting to call super.onProcess(). Consider
+ * carefully whether you should call super.onProcess() or not,
+ * especially for {@link org.apache.click.control.Container}s which by default
+ * call onProcess on all their child controls as well.
+ *
+ * @return true to continue Page event processing or false otherwise
+ */
+ public boolean onProcess();
+
+ /**
+ * The on render event handler. This event handler is invoked prior to the
+ * control being rendered, and is useful for providing pre rendering logic.
+ *
+ * The on render method is typically used to populate tables performing some
+ * database intensive operation. By putting the intensive operations in the
+ * on render method they will not be performed if the user navigates away
+ * to a different page.
+ *
+ * {@link org.apache.click.control.Container} implementations should recursively
+ * invoke the onRender method on each of their child controls ensuring that
+ * all controls receive this event.
+ *
+ * Please note: a common problem when overriding onRender in
+ * subclasses is forgetting to call super.onRender(). Consider
+ * carefully whether you should call super.onRender() or not,
+ * especially for {@link org.apache.click.control.Container}s which by default
+ * call onRender on all their child controls as well.
+ */
+ public void onRender();
+
+ /**
+ * The on destroy request event handler. Control classes should use this
+ * method to add any resource clean up code.
+ *
+ * This method is guaranteed to be called before the Page object reference
+ * goes out of scope and is available for garbage collection.
+ *
+ * {@link org.apache.click.control.Container} implementations should recursively
+ * invoke the onDestroy method on each of their child controls ensuring that
+ * all controls receive this event.
+ *
+ * Please note: a common problem when overriding onDestroy in
+ * subclasses is forgetting to call super.onDestroy(). Consider
+ * carefully whether you should call super.onDestroy() or not,
+ * especially for {@link org.apache.click.control.Container}s which by default
+ * call onDestroy on all their child controls as well.
+ */
+ public void onDestroy();
+
+ /**
+ * Render the control's HTML representation to the specified buffer. The
+ * control's {@link java.lang.Object#toString()} method should delegate the
+ * rendering to the render method for improved performance.
+ *
+ * An example implementation:
+ *
+ *
+ * @param buffer the specified buffer to render the control's output to
+ */
+ public void render(HtmlStringBuffer buffer);
+
+ /**
+ * Returns true if this control has any
+ * Behaviors registered, false otherwise.
+ *
+ * @return true if this control has any
+ * Behaviors registered, false otherwise
+ */
+ public boolean hasBehaviors();
+
+ /**
+ * Returns the list of behaviors for this control.
+ *
+ * @return the list with this control behaviors.
+ */
+ public Set getBehaviors();
+
+ /**
+ * Returns true if this control is an Ajax target, false
+ * otherwise.
+ *
+ * In order for a Control to be considered as an Ajax target it must be
+ * registered through {@link org.apache.click.ControlRegistry#registerAjaxTarget(org.apache.click.Control) ControlRegistry.registerAjaxTarget}.
+ *
+ * When the Click handles an Ajax request it iterates the Controls
+ * registered with the {@link org.apache.click.ControlRegistry ControlRegistry}
+ * and checks if one of them is the Ajax target by calling
+ * {@link #isAjaxTarget(org.openidentityplatform.openam.click.Context) isAjaxTarget}. If isAjaxTarget
+ * returns true, Click will process that Control's {@link #getBehaviors() behaviors}.
+ *
+ * Please note: there can only be one target control, so the first
+ * Control that is identified as the Ajax target will be processed, the other
+ * controls will be skipped.
+ *
+ * The most common way to check whether a Control is the Ajax target is to
+ * check if its {@link #getId ID} is available as a request parameter:
+ *
+ *
+ *
+ * Not every scenario can be covered through an ID attribute though. For example
+ * if an ActionLink is rendered multiple times on the same page, it cannot have an
+ * ID attribute, as that would lead to duplicate IDs, which isn't allowed by
+ * the HTML specification. Control implementations has to cater for how the
+ * control will be targeted. In the case of ActionLink it might check against
+ * its id, and if that isn't available check against its name.
+ *
+ * @param context the request context
+ * @return true if this control is an Ajax target, false
+ * otherwise
+ */
+ public boolean isAjaxTarget(Context context);
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/ControlRegistry.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/ControlRegistry.java
new file mode 100644
index 0000000000..ecc271bb5e
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/ControlRegistry.java
@@ -0,0 +1,525 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.openidentityplatform.openam.click.service.ConfigService;
+import org.openidentityplatform.openam.click.service.LogService;
+import org.apache.commons.lang.Validate;
+
+/**
+ * Provides a centralized registry where Controls can be registered and interact
+ * with the Click runtime.
+ *
+ * The primary use of the ControlRegistry is for Controls to register themselves
+ * as potential targets of Ajax requests
+ * (If a control is an Ajax request target, it's onProcess()
+ * method is invoked while other controls are not processed).
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
Register Control as an Ajax Target
+ * Below is an example of a Control registering itself as an Ajax target:
+ *
+ *
+ * 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);
+ * }
+ * }
+ *
+ *
Register Interceptor
+ * Below is an example of a Container registering a Behavior in order to intercept
+ * and decorate its child controls:
+ *
+ *
+ * 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;
+ * }
+ * }
+ */
+public class ControlRegistry {
+
+ // Constants --------------------------------------------------------------
+
+ /** The thread local registry holder. */
+ private static final ThreadLocal THREAD_LOCAL_REGISTRY_STACK =
+ new ThreadLocal<>();
+
+ // Variables --------------------------------------------------------------
+
+ /** The set of Ajax target controls. */
+ Set ajaxTargetControls;
+
+ /** The list of registered interceptors. */
+ List 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.
+ *
+ * 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() {
+ ControlRegistry.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(Context)} method returns true.
+ * Once a target control is identified, Click invokes its
+ * {@link Control#onProcess()} method.
+ *
+ * This method serves a dual purpose as all controls registered here
+ * will also have their Behaviors (if any) processed. Processing
+ * {@link 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");
+
+ ControlRegistry.InterceptorHolder interceptorHolder = new ControlRegistry.InterceptorHolder(source, controlInterceptor);
+
+ // Guard against adding duplicate interceptors
+ List 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 (ControlRegistry.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 (ControlRegistry.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 (ControlRegistry.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 getAjaxTargetControls() {
+ if (ajaxTargetControls == null) {
+ ajaxTargetControls = new LinkedHashSet();
+ }
+ 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 getInterceptors() {
+ if (interceptors == null) {
+ interceptors = new ArrayList<>();
+ }
+ 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() {
+ ControlRegistry.RegistryStack registryStack = getRegistryStack();
+ ControlRegistry controlRegistry = registryStack.pop();
+
+ if (registryStack.isEmpty()) {
+ THREAD_LOCAL_REGISTRY_STACK.set(null);
+ }
+
+ return controlRegistry;
+ }
+
+ static ControlRegistry.RegistryStack getRegistryStack() {
+ ControlRegistry.RegistryStack registryStack = THREAD_LOCAL_REGISTRY_STACK.get();
+
+ if (registryStack == null) {
+ registryStack = new ControlRegistry.RegistryStack(2);
+ THREAD_LOCAL_REGISTRY_STACK.set(registryStack);
+ }
+
+ return registryStack;
+ }
+
+ /**
+ * Provides an unsynchronized Stack.
+ */
+ static class RegistryStack extends ArrayList {
+
+ /** 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 ControlRegistry.InterceptorHolder)) {
+ return false;
+ }
+
+ //3. Cast the argument to the correct type.
+ ControlRegistry.InterceptorHolder that = (ControlRegistry.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;
+ }
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/Page.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/Page.java
new file mode 100644
index 0000000000..aa65de9e1d
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/Page.java
@@ -0,0 +1,1362 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.openidentityplatform.openam.click.element.Element;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.apache.click.util.Format;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+import org.openidentityplatform.openam.click.util.PageImports;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Provides the Page request event handler class.
+ *
+ * The Page class plays a central role in Click applications defining how the
+ * application's pages are processed and rendered. All application pages
+ * must extend the base Page class, and provide a no arguments constructor.
+ *
+ *
Page Execution Sequence
+ *
+ * The default Page execution path for a GET request is:
+ *
+ *
+ * no-args constructor invoked to create a new Page instance.
+ * At this point no dependencies have been injected into the Page, and any
+ * request information is not available. You should put any "static"
+ * page initialization code, which doesn't depend upon request information,
+ * in the constructor. This will enable subclasses to have this code
+ * automatically initialized when they are created.
+ *
+ *
+ * {@link #format} property is set
+ *
+ *
+ * {@link #headers} property is set
+ *
+ *
+ * {@link #path} property is set
+ *
+ *
+ * {@link #onSecurityCheck()} method called to check whether the page should
+ * be processed. This method should return true if the Page should continue
+ * to be processed, or false otherwise.
+ *
+ *
+ * {@link #onInit()} method called to complete the initialization of the page
+ * after all the dependencies have been set. This is where you should put
+ * any "dynamic" page initialization code which depends upon the request or any
+ * other dependencies.
+ *
+ * Form and field controls must be fully initialized by the time this method
+ * has completed.
+ *
+ *
+ * ClickServlet processes all the page {@link #controls}
+ * calling their {@link org.apache.click.Control#onProcess()} method. If any of these
+ * controls return false, continued control and page processing will be aborted.
+ *
+ *
+ * {@link #onGet()} method called for any additional GET related processing.
+ *
+ * Form and field controls should NOT be created or initialized at this
+ * point as the control processing stage has already been completed.
+ *
+ *
+ * {@link #onRender()} method called for any pre-render processing. This
+ * method is often use to perform database queries to load information for
+ * rendering tables.
+ *
+ * Form and field controls should NOT be created or initialized at this
+ * point as the control processing stage has already been completed.
+ *
+ *
+ * ClickServlet renders the page merging the {@link #model} with the
+ * Velocity template defined by the {@link #getTemplate()} property.
+ *
+ *
+ * {@link #onDestroy()} method called to clean up any resources. This method
+ * is guaranteed to be called, even if an exception occurs. You can use
+ * this method to close resources like database connections or Hibernate
+ * sessions.
+ *
+ *
+ *
+ * For POST requests the default execution path is identical, except the
+ * {@link #onPost()} method is called instead of {@link #onGet()}. The POST
+ * request page execution sequence is illustrated below:
+ *
+ *
+ *
+ *
+ * A good way to see the page event execution order is to view the log when
+ * the application mode is set to trace:
+ *
+ *
+ *
+ * When a Velocity template is rendered the ClickServlet uses Pages:
+ *
+ *
{@link #getTemplate()} to find the Velocity template.
+ *
{@link #model} to populate the Velocity Context
+ *
{@link #format} to add to the Velocity Context
+ *
{@link #getContentType()} to set as the HttpServletResponse content type
+ *
{@link #headers} to set as the HttpServletResponse headers
+ *
+ *
+ * These Page properties are also used when rendering JSP pages.
+ */
+public class Page implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The global page messages bundle name: click-page.
+ */
+ public static final String PAGE_MESSAGES = "click-page";
+
+ /**
+ * The Page action request parameter: "pageAction".
+ */
+ public final static String PAGE_ACTION = "pageAction";
+
+ // Instance Variables -----------------------------------------------------
+
+ /** The list of page controls. */
+ protected List controls;
+
+ /**
+ * The list of page HTML HEAD elements including: Javascript imports,
+ * Css imports, inline Javascript and inline Css.
+ */
+ protected List headElements;
+
+ /** The Velocity template formatter object. */
+ protected Format format;
+
+ /** The forward path. */
+ protected String forward;
+
+ /** The HTTP response headers. */
+ protected Map headers;
+
+ /** The map of localized page resource messages. **/
+ protected transient Map messages;
+
+ /**
+ * The page model. For Velocity templates the model is used to populate the
+ * Velocity context. For JSP pages the model values are set as named
+ * request attributes.
+ */
+ protected Map model = new HashMap();
+
+ /** The Page header imports. */
+ protected transient PageImports pageImports;
+
+ /** The path of the page template to render. */
+ protected String path;
+
+ /** The redirect path. */
+ protected String redirect;
+
+ /**
+ * The page is stateful and should be saved to the users HttpSession
+ * between requests, default value is false.
+ *
+ * @deprecated stateful pages are not supported anymore, use stateful
+ * Controls instead
+ */
+ protected boolean stateful;
+
+ /** The path of the page border template to render.*/
+ protected String template;
+
+ /**
+ * Indicates whether Control head elements should be included in the
+ * page template, default value is true.
+ */
+ protected boolean includeControlHeadElements = true;
+
+ // Event Handlers ---------------------------------------------------------
+
+ /**
+ * The on Security Check event handler. This event handler is invoked after
+ * the pages constructor has been called and all the page properties have
+ * been set.
+ *
+ * Security check provides the Page an opportunity to check the users
+ * security credentials before processing the Page.
+ *
+ * If security check returns true the Page is processed as
+ * normal. If the method returns then no other event handlers are invoked
+ * (except onDestroy() and no page controls are processed.
+ *
+ * If the method returns false, the forward or redirect property should be
+ * set to send the request to another Page.
+ *
+ * By default this method returns true, subclass may override this method
+ * to provide their security authorization/authentication mechanism.
+ *
+ * @return true by default, subclasses may override this method
+ */
+ public boolean onSecurityCheck() {
+ return true;
+ }
+
+ /**
+ * The on Initialization event handler. This event handler is invoked after
+ * the {@link #onInit()} method has been called.
+ *
+ * Subclasses should place any initialization code which has dependencies
+ * on the context or other properties in this method. Generally light
+ * weight initialization code should be placed in the Pages constructor.
+ *
+ * Time consuming operations such as fetching the results of a database
+ * query should not be placed in this method. These operations should be
+ * performed in the {@link #onRender()}, {@link #onGet()} or
+ * {@link #onPost()} methods so that other event handlers may take
+ * alternative execution paths without performing these expensive operations.
+ *
+ * Please Note however the qualifier for the previous statement is
+ * that all form and field controls must be fully initialized before they
+ * are processed, which is after the onInit() method has
+ * completed. After this point their onProcess() methods will be
+ * invoked by the ClickServlet.
+ *
+ * Select controls in particular must have their option list values populated
+ * before the form is processed otherwise field validations cannot be performed.
+ *
+ * For initializing page controls the best practice is to place all the
+ * control creation code in the pages constructor, and only place any
+ * initialization code in the onInit() method which has an external
+ * dependency to the context or some other object. By following this practice
+ * it is easy to see what code is "design time" initialization code and what
+ * is "runtime initialization code".
+ *
+ * When subclassing pages which also use the onInit() method is
+ * is critical you call the super.onInit() method first, for
+ * example:
+ *
+ */
+ public void onInit() {
+ }
+
+ /**
+ * The on Get request event handler. This event handler is invoked if the
+ * HTTP request method is "GET".
+ *
+ * The event handler is invoked after {@link #onSecurityCheck()} has been
+ * called and all the Page {@link #controls} have been processed. If either
+ * the security check or one of the controls cancels continued event
+ * processing the onGet() method will not be invoked.
+ *
+ *
Important Note
+ *
+ * Form and field controls should NOT be created
+ * or initialized at this point as the control processing stage has already
+ * been completed. Select option list values should also be populated
+ * before the control processing stage is performed so that they can
+ * validate the submitted values.
+ */
+ public void onGet() {
+ }
+
+ /**
+ * The on Post request event handler. This event handler is invoked if the
+ * HTTP request method is "POST".
+ *
+ * The event handler is invoked after {@link #onSecurityCheck()} has been
+ * called and all the Page {@link #controls} have been processed. If either
+ * the security check or one of the controls cancels continued event
+ * processing the onPost() method will not be invoked.
+ *
+ *
Important Note
+ *
+ * Form and field controls should NOT be created
+ * or initialized at this point as the control processing stage has already
+ * been completed. Select option list values should also be populated
+ * before the control processing stage is performed so that they can
+ * validate the submitted values.
+ */
+ public void onPost() {
+ }
+
+ /**
+ * The on render event handler. This event handler is invoked prior to the
+ * page being rendered.
+ *
+ * This method will not be invoked if either the security check or one of
+ * the controls cancels continued event processing.
+ *
+ * The on render method is typically used to populate tables performing some
+ * database intensive operation. By putting the intensive operations in the
+ * on render method they will not be performed if the user navigates away
+ * to a different page.
+ *
+ * If you have code which you are using in both the onGet() and
+ * onPost() methods, use the onRender() method instead.
+ *
+ *
Important Note
+ *
+ * Form and field controls should NOT be created
+ * or initialized at this point as the control processing stage has already
+ * been completed. Select option list values should also be populated
+ * before the control processing stage is performed so that they can
+ * validate the submitted values.
+ */
+ public void onRender() {
+ }
+
+ /**
+ * The on Destroy request event handler. Subclasses may override this method
+ * to add any resource clean up code.
+ *
+ * This method is guaranteed to be called before the Page object reference
+ * goes out of scope and is available for garbage collection.
+ */
+ public void onDestroy() {
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * Add the control to the page. The control will be added to the page model
+ * using the control name as the key. The Controls parent property will
+ * also be set to the page instance.
+ *
+ * Please note: if the page contains a control with the same name as
+ * the given control, that control will be replaced by the given control.
+ * If a control has no name defined it cannot be replaced.
+ *
+ * @param control the control to add to the page
+ * @throws IllegalArgumentException if the control is null or if the name
+ * of the control is not defined
+ */
+ public void addControl(Control control) {
+ if (control == null) {
+ throw new IllegalArgumentException("Null control parameter");
+ }
+ if (StringUtils.isBlank(control.getName())) {
+ throw new IllegalArgumentException("Control name not defined: "
+ + control.getClass());
+ }
+
+ // Check if page already contains a named value
+ Object currentValue = getModel().get(control.getName());
+ if (currentValue != null && currentValue instanceof Control) {
+ Control currentControl = (Control) currentValue;
+ replaceControl(currentControl, control);
+ return;
+ }
+
+ // Note: set parent first as setParent might veto further processing
+ control.setParent(this);
+
+ getControls().add(control);
+ addModel(control.getName(), control);
+ }
+
+ /**
+ * Remove the control from the page. The control will be removed from the
+ * pages model and the control parent property will be set to null.
+ *
+ * @param control the control to remove
+ * @throws IllegalArgumentException if the control is null, or if the name
+ * of the control is not defined
+ */
+ public void removeControl(Control control) {
+ if (control == null) {
+ throw new IllegalArgumentException("Null control parameter");
+ }
+ if (StringUtils.isBlank(control.getName())) {
+ throw new IllegalArgumentException("Control name not defined");
+ }
+
+ getControls().remove(control);
+ getModel().remove(control.getName());
+
+ control.setParent(null);
+ }
+
+ /**
+ * Return the list of page Controls.
+ *
+ * @return the list of page Controls
+ */
+ public List getControls() {
+ if (controls == null) {
+ controls = new ArrayList();
+ }
+ return controls;
+ }
+
+ /**
+ * Return true if the page has any controls defined.
+ *
+ * @return true if the page has any controls defined
+ */
+ public boolean hasControls() {
+ return (controls != null) && !controls.isEmpty();
+ }
+
+ /**
+ * Return the request context of the page.
+ *
+ * @return the request context of the page
+ */
+ public Context getContext() {
+ return Context.getThreadLocalContext();
+ }
+
+ /**
+ * Return the HTTP response content type. By default this method returns
+ * "text/html".
+ *
+ * If the request specifies a character encoding via
+ * If {@link jakarta.servlet.ServletRequest#getCharacterEncoding()}
+ * then this method will return "text/html; charset=encoding".
+ *
+ * The ClickServlet uses the pages content type for setting the
+ * HttpServletResponse content type.
+ *
+ * @return the HTTP response content type
+ */
+ public String getContentType() {
+ String charset = getContext().getRequest().getCharacterEncoding();
+
+ if (charset == null) {
+ return "text/html";
+
+ } else {
+ return "text/html; charset=" + charset;
+ }
+ }
+
+ /**
+ * Return the Velocity template formatter object.
+ *
+ * The ClickServlet adds the format object to the Velocity context using
+ * the key "format" so that it can be used in the page template.
+ *
+ * @return the Velocity template formatter object
+ */
+ public Format getFormat() {
+ return format;
+ }
+
+ /**
+ * Set the Velocity template formatter object.
+ *
+ * @param value the Velocity template formatter object.
+ */
+ public void setFormat(Format value) {
+ format = value;
+ }
+
+ /**
+ * Return the path to forward the request to.
+ *
+ * If the {@link #forward} property is not null it will be used to forward
+ * the request to in preference to rendering the template defined by the
+ * {@link #path} property. The request is forwarded using the
+ * RequestDispatcher.
+ *
+ * See also {@link #getPath()}, {@link #getRedirect()}
+ *
+ * @return the path to forward the request to
+ */
+ public String getForward() {
+ return forward;
+ }
+
+ /**
+ * Set the path to forward the request to.
+ *
+ * If the {@link #forward} property is not null it will be used to forward
+ * the request to in preference to rendering the template defined by the
+ * {@link #path} property. The request is forwarded using the Servlet
+ * RequestDispatcher.
+ *
+ * If forward paths start with a "/"
+ * character the forward path is
+ * relative to web applications root context, otherwise the path is
+ * relative to the requests current location.
+ *
+ * For example given a web application deployed to context mycorp
+ * with the pages:
+ *
+ *
+ * To forward to the customer search.htm page from
+ * the web app root you could set forward as
+ * setForward("/customer/search.htm")
+ * or setForward("customer/search.htm").
+ *
+ * If a user was currently viewing the add-customer.htm
+ * to forward to customer details.htm you could
+ * set forward as
+ * setForward("/customer/details.htm")
+ * or setForward("../details.htm").
+ *
+ * See also {@link #setPath(String)}, {@link #setRedirect(String)}
+ *
+ * @param value the path to forward the request to
+ */
+ public void setForward(String value) {
+ forward = value;
+ }
+
+ /**
+ * The Page instance to forward the request to. The given Page object
+ * must have a valid {@link #path} defined, as the {@link #path} specifies
+ * the location to forward to.
+ *
+ * @see #setForward(java.lang.String)
+ *
+ * @param page the Page object to forward the request to.
+ */
+ public void setForward(Page page) {
+ if (page == null) {
+ throw new IllegalArgumentException("Null page parameter");
+ }
+ if (page.getPath() == null) {
+ throw new IllegalArgumentException("Page has no path defined");
+ }
+ setForward(page.getPath());
+ getContext().setRequestAttribute(ClickServlet.FORWARD_PAGE, page);
+ }
+
+ /**
+ * Set the request to forward to the given page class.
+ *
+ * @see #setForward(java.lang.String)
+ *
+ * @param pageClass the class of the Page to forward the request to
+ * @throws IllegalArgumentException if the Page Class is not configured
+ * with a unique path
+ */
+ public void setForward(Class extends Page> pageClass) {
+ String target = getContext().getPagePath(pageClass);
+
+ // If page class maps to a jsp, convert to htm which allows ClickServlet
+ // to process the redirect
+ if (target != null && target.endsWith(".jsp")) {
+ target = StringUtils.replaceOnce(target, ".jsp", ".htm");
+ }
+ setForward(target);
+ }
+
+ /**
+ * Return the map of HTTP header to be set in the HttpServletResponse.
+ *
+ * @return the map of HTTP header to be set in the HttpServletResponse
+ */
+ public Map getHeaders() {
+ if (headers == null) {
+ headers = new HashMap();
+ }
+ return headers;
+ }
+
+ /**
+ * Return true if the page has headers, false otherwise.
+ *
+ * @return true if the page has headers, false otherwise
+ */
+ public boolean hasHeaders() {
+ return headers != null && !headers.isEmpty();
+ }
+
+ /**
+ * Set the named header with the given value. Value can be either a String,
+ * Date or Integer.
+ *
+ * @param name the name of the header
+ * @param value the value of the header, either a String, Date or Integer
+ */
+ public void setHeader(String name, Object value) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null header name parameter");
+ }
+
+ getHeaders().put(name, value);
+ }
+
+ /**
+ * Set the map of HTTP header to be set in the HttpServletResponse.
+ *
+ * @param value the map of HTTP header to be set in the HttpServletResponse
+ */
+ public void setHeaders(Map value) {
+ headers = value;
+ }
+
+ /**
+ * @deprecated use the new {@link #getHeadElements()} instead
+ *
+ * @return the HTML includes statements for the control stylesheet and
+ * JavaScript files
+ */
+ public final String getHtmlImports() {
+ throw new UnsupportedOperationException("Use getHeadElements instead");
+ }
+
+ /**
+ * Return the list of HEAD {@link org.apache.click.element.Element elements}
+ * to be included in the page. Example HEAD elements include
+ * {@link org.apache.click.element.JsImport JsImport},
+ * {@link org.apache.click.element.JsScript JsScript},
+ * {@link org.apache.click.element.CssImport CssImport} and
+ * {@link org.apache.click.element.CssStyle CssStyle}.
+ *
+ * Pages can contribute their own list of HEAD elements by overriding
+ * this method.
+ *
+ * The recommended approach when overriding this method is to use
+ * lazy loading to ensure the HEAD elements are only added
+ * once and when needed. For example:
+ *
+ *
+ * public MyPage extends Page {
+ *
+ * public List getHeadElements() {
+ * // Use lazy loading to ensure the JS is only added the
+ * // first time this method is called.
+ * if (headElements == null) {
+ * // Get the head elements from the super implementation
+ * headElements = super.getHeadElements();
+ *
+ * // Include the page's external Javascript resource
+ * JsImport jsImport = new JsImport("/mycorp/js/mypage.js");
+ * headElements.add(jsImport);
+ *
+ * // Include the page's external Css resource
+ * CssImport cssImport = new CssImport("/mycorp/js/mypage.css");
+ * headElements.add(cssImport);
+ * }
+ * return headElements;
+ * }
+ * }
+ *
+ * Alternatively one can add the HEAD elements in the Page constructor:
+ *
+ *
+ *
+ * One can also add HEAD elements from event handler methods such as
+ * {@link #onInit()}, {@link #onGet()}, {@link #onPost()}, {@link #onRender()}
+ * etc.
+ *
+ * The order in which JS and CSS files are included will be preserved in the
+ * page.
+ *
+ * Note: this method must never return null. If no HEAD elements
+ * are available this method must return an empty {@link java.util.List}.
+ *
+ * Also note: a common problem when overriding getHeadElements in
+ * subclasses is forgetting to call super.getHeadElements. Consider
+ * carefully whether you should call super.getHeadElements or not.
+ *
+ * @return the list of HEAD elements to be included in the page
+ */
+ public List getHeadElements() {
+ if (headElements == null) {
+ headElements = new ArrayList(2);
+ }
+ return headElements;
+ }
+
+ /**
+ * Return the localized Page resource message for the given resource
+ * name or null if not found. The resource message returned will use the
+ * Locale obtained from the Context.
+ *
+ * Pages can define text properties files to store localized messages. These
+ * properties files must be stored on the Page class path with a name
+ * matching the class name. For example:
+ *
+ * The page class:
+ *
+ * package com.mycorp.pages;
+ *
+ * public class Login extends Page {
+ * ..
+ *
+ * The page class property filenames and their path:
+ *
+ *
+ * Page messages can also be defined in the optional global messages
+ * bundle:
+ *
+ *
+ * /click-page.properties
+ *
+ * To define global page messages simply add click-page.properties
+ * file to your application's class path. Message defined in this properties
+ * file will be available to all of your application pages.
+ *
+ * Note messages in your page class properties file will override any
+ * messages in the global click-page.properties file.
+ *
+ * Page messages can be accessed directly in the page template using
+ * the $messages reference. For examples:
+ *
+ *
+ * $messages.title
+ *
+ * Please see the {@link org.apache.click.util.MessagesMap} adaptor for more
+ * details.
+ *
+ * @param name resource name of the message
+ * @return the named localized message for the page or null if no message
+ * was found
+ */
+ public String getMessage(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+ return getMessages().get(name);
+ }
+
+ /**
+ * Return the formatted page message for the given resource name and
+ * message format arguments or null if no message was found. The resource
+ * message returned will use the Locale obtained from the Context.
+ *
+ * {@link #getMessage(java.lang.String)} is invoked to retrieve the message
+ * for the specified name.
+ *
+ * @param name resource name of the message
+ * @param args the message arguments to format
+ * @return the named localized message for the page or null if no message
+ * was found
+ */
+ public String getMessage(String name, Object... args) {
+ String value = getMessage(name);
+
+ return MessageFormat.format(value, args);
+ }
+
+ /**
+ * Return a Map of localized messages for the Page. The messages returned
+ * will use the Locale obtained from the Context.
+ *
+ * @see #getMessage(String)
+ *
+ * @return a Map of localized messages for the Page
+ * @throws IllegalStateException if the context for the Page has not be set
+ */
+ public Map getMessages() {
+ if (messages == null) {
+ if (getContext() != null) {
+ messages = getContext().createMessagesMap(getClass(), PAGE_MESSAGES);
+
+ } else {
+ String msg = "Context not set cannot initialize messages";
+ throw new IllegalStateException(msg);
+ }
+ }
+ return messages;
+ }
+
+ /**
+ * Add the named object value to the Pages model map.
+ *
+ * Please note: if the Page contains an object with a matching name,
+ * that object will be replaced by the given value.
+ *
+ * @param name the key name of the object to add
+ * @param value the object to add
+ * @throws IllegalArgumentException if the name or value parameters are
+ * null
+ */
+ public void addModel(String name, Object value) {
+ if (name == null) {
+ String msg = "Cannot add null parameter name to "
+ + getClass().getName() + " model";
+ throw new IllegalArgumentException(msg);
+ }
+ if (value == null) {
+ String msg = "Cannot add null " + name + " parameter "
+ + "to " + getClass().getName() + " model";
+ throw new IllegalArgumentException(msg);
+ }
+
+ getModel().put(name, value);
+ }
+
+ /**
+ * Return the Page's model map. The model is used populate the
+ * Velocity Context with is merged with the page template before rendering.
+ *
+ * @return the Page's model map
+ */
+ public Map getModel() {
+ return model;
+ }
+
+ /**
+ * Return the Page header imports.
+ *
+ * PageImports are used define the CSS and JavaScript imports and blocks
+ * to be included in the page template.
+ *
+ * The PageImports object will be included in the Page template when the
+ * following methods are invoked:
+ *
+ *
{@link ClickServlet#createTemplateModel(Page)} - for template pages
+ *
{@link ClickServlet#setRequestAttributes(Page)} - for JSP pages
+ *
+ *
+ * If you need to tailor the page imports rendered, override this method
+ * and modify the PageImports object returned.
+ *
+ * If you need to create a custom PageImports, override the method
+ * {@link ClickServlet#createPageImports(org.openidentityplatform.openam.click.Page)}
+ *
+ * @deprecated use the new {@link #getHeadElements()} instead
+ *
+ * @return the Page header imports
+ */
+ public PageImports getPageImports() {
+ return pageImports;
+ }
+
+ /**
+ * Set the Page header imports.
+ *
+ * PageImports are used define the CSS and JavaScript imports and blocks
+ * to be included in the page template.
+ *
+ * The PageImports references will be included in the Page model when the
+ * following methods are invoked:
+ *
+ *
{@link ClickServlet#createTemplateModel(Page)} - for template pages
+ *
{@link ClickServlet#setRequestAttributes(Page)} - for JSP pages
+ *
+ *
+ * If you need to tailor the page imports rendered, override the
+ * {@link #getPageImports()} method and modify the PageImports object
+ * returned.
+ *
+ * If you need to create a custom PageImports, override the method
+ * {@link ClickServlet#createPageImports(org.openidentityplatform.openam.click.Page)}
+ *
+ * @deprecated use the new {@link #getHeadElements()} instead
+ *
+ * @param pageImports the new pageImports instance to set
+ */
+ public void setPageImports(PageImports pageImports) {
+ this.pageImports = pageImports;
+ }
+
+ /**
+ * Return the path of the Template or JSP to render.
+ *
+ * If this method returns null, Click will not perform any rendering.
+ * This is useful when you want to stream or write directly to the
+ * HttpServletResponse.
+ *
+ * See also {@link #getForward()}, {@link #getRedirect()}
+ *
+ * @return the path of the Template or JSP to render
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Set the path of the Template or JSP to render.
+ *
+ * By default Click will set the path to the requested page url. Meaning
+ * if the url /edit-customer.htm is requested, path will be set
+ * to /edit-customer.htm.
+ *
+ * Here is an example if you want to change the path to a different Template:
+ *
+ *
+ *
+ * If path is set to null, Click will not perform any rendering.
+ * This is useful when you want to stream or write directly to the
+ * HttpServletResponse.
+ *
+ * See also {@link #setForward(String)}, {@link #setRedirect(String)}
+ *
+ * @param value the path of the Template or JSP to render
+ */
+ public void setPath(String value) {
+ path = value;
+ }
+
+ /**
+ * Return the path to redirect the request to.
+ *
+ * If the {@link #redirect} property is not null it will be used to redirect
+ * the request in preference to {@link #forward} or {@link #path} properties.
+ * The request is redirected to using the HttpServletResponse.setRedirect()
+ * method.
+ *
+ * See also {@link #getForward()}, {@link #getPath()}
+ *
+ * @return the path to redirect the request to
+ */
+ public String getRedirect() {
+ return redirect;
+ }
+
+ /**
+ * Return true if the page is stateful and should be saved in the users
+ * HttpSession between requests, default value is false.
+ *
+ * @deprecated stateful pages are not supported anymore, use stateful
+ * Controls instead
+ *
+ * @return true if the page is stateful and should be saved in the users
+ * session
+ */
+ public boolean isStateful() {
+ return stateful;
+ }
+
+ /**
+ * Set whether the page is stateful and should be saved in the users
+ * HttpSession between requests.
+ *
+ * Click will synchronize on the page instance. This ensures that if
+ * multiple requests arrive from the same user for the page, only one
+ * request can access the page at a time.
+ *
+ * Stateful pages are stored in the HttpSession using the key
+ * page.getClass().getName().
+ *
+ * It is worth noting that Click checks a Page's stateful property after
+ * each request. Thus it becomes possible to enable a stateful Page for a
+ * number of requests and then setting it to false again at which
+ * point Click will remove the Page from the HttpSession, freeing up memory
+ * for the server.
+ *
+ * @deprecated stateful pages are not supported anymore, use stateful
+ * Controls instead
+ *
+ * @param stateful the flag indicating whether the page should be saved
+ * between user requests
+ */
+ public void setStateful(boolean stateful) {
+ this.stateful = stateful;
+ if (isStateful()) {
+ getContext().getSession();
+ }
+ }
+
+ /**
+ * Return true if the Control head elements should be included in the page
+ * template, false otherwise. Default value is true.
+ *
+ * @see #setIncludeControlHeadElements(boolean)
+ *
+ * @return true if the Control head elements should be included in the page
+ * template, false otherwise
+ */
+ public boolean isIncludeControlHeadElements() {
+ return includeControlHeadElements;
+ }
+
+ /**
+ * Set whether the Control head elements should be included in the page
+ * template.
+ *
+ * By setting this value to false, Click won't include Control's
+ * {@link #getHeadElements() head elements}, however the Page head elements
+ * will still be included.
+ *
+ * This allows one to create a single JavaScript and CSS resource file for
+ * the entire Page which increases performance, since the browser only has
+ * to load one resource, instead of multiple resources.
+ *
+ * Below is an example:
+ *
+ *
+ * public class HomePage extends Page {
+ *
+ * private Form form = new Form("form");
+ *
+ * public HomePage() {
+ * // Indicate that Controls should not import their head elements
+ * setIncludeControlHeadElements(false);
+ *
+ * form.add(new EmailField("email");
+ * addControl(form);
+ * }
+ *
+ * // Include the Page JavaScript and CSS resources
+ * public List getHeadElements() {
+ * if (headElements == null) {
+ * headElements = super.getHeadElements();
+ *
+ * // Include the Page CSS resource. This resource should combine
+ * // all the CSS necessary for the page
+ * headElements.add(new CssImport("/assets/css/home-page.css"));
+ *
+ * // Include the Page JavaScript resource. This resource should
+ * // combine all the JavaScript necessary for the page
+ * headElements.add(new JsImport("/assets/js/home-page.js"));
+ * }
+ * return headElements;
+ * }
+ * }
+ *
+ * @param includeControlHeadElements flag indicating whether Control
+ * head elements should be included in the page
+ */
+ public void setIncludeControlHeadElements(boolean includeControlHeadElements) {
+ this.includeControlHeadElements = includeControlHeadElements;
+ }
+
+ /**
+ * Set the location to redirect the request to.
+ *
+ * If the {@link #redirect} property is not null it will be used to redirect
+ * the request in preference to the {@link #forward} and {@link #path}
+ * properties. The request is redirected using the HttpServletResponse.setRedirect()
+ * method.
+ *
+ * If the redirect location begins with a "/"
+ * character the redirect location will be prefixed with the web applications
+ * context path. Note if the given location is already prefixed
+ * with the context path, Click won't add it a second time.
+ *
+ * For example if an application is deployed to the context
+ * "mycorp" calling
+ * setRedirect("/customer/details.htm")
+ * will redirect the request to:
+ * "/mycorp/customer/details.htm"
+ *
+ * If the redirect location does not begin with a "/"
+ * character the redirect location will be used as specified. Thus if the
+ * location is http://somehost.com/myapp/customer.jsp,
+ * Click will redirect to that location.
+ *
+ * JSP note: when redirecting to a JSP template keep in mind that the
+ * JSP template won't be processed by Click, as ClickServlet is mapped to
+ * *.htm. Instead JSP templates are processed by the Servlet
+ * container JSP engine.
+ *
+ * So if you have a situation where a Page Class
+ * (Customer.class) is mapped to the JSP
+ * ("/customer.jsp") and you want to redirect to
+ * Customer.class, you could either redirect to
+ * ("/customer.htm") or
+ * use the alternative redirect utility {@link #setRedirect(java.lang.Class)}.
+ *
+ * Please note that Click will url encode the location by invoking
+ * response.encodeRedirectURL(location) before redirecting.
+ *
+ * See also {@link #setRedirect(java.lang.String, java.util.Map)},
+ * {@link #setForward(String)}, {@link #setPath(String)}
+ *
+ * @param location the path to redirect the request to
+ */
+ public void setRedirect(String location) {
+ setRedirect(location, null);
+ }
+
+ /**
+ * Set the request to redirect to the give page class.
+ *
+ * @see #setRedirect(java.lang.String)
+ *
+ * @param pageClass the class of the Page to redirect the request to
+ * @throws IllegalArgumentException if the Page Class is not configured
+ * with a unique path
+ */
+ public void setRedirect(Class extends Page> pageClass) {
+ setRedirect(pageClass, null);
+ }
+
+ /**
+ * Set the request to redirect to the given location and append
+ * the map of request parameters to the location URL.
+ *
+ * The map keys will be used as the request parameter names and the map
+ * values will be used as the request parameter values. For example:
+ *
+ *
+ *
+ * @see #setRedirect(java.lang.String)
+ *
+ * @param location the path to redirect the request to
+ * @param params the map of request parameter name and value pairs
+ */
+ public void setRedirect(String location, Map params) {
+ Context context = getContext();
+ if (StringUtils.isNotBlank(location)) {
+ if (location.charAt(0) == '/') {
+ String contextPath = context.getRequest().getContextPath();
+
+ // Guard against adding duplicate context path
+ if (!location.startsWith(contextPath + '/')) {
+ location = contextPath + location;
+ }
+ }
+ }
+
+ if (params != null && !params.isEmpty()) {
+ HtmlStringBuffer buffer = new HtmlStringBuffer();
+
+ Iterator extends Map.Entry> i = params.entrySet().iterator();
+ while (i.hasNext()) {
+ Map.Entry entry = i.next();
+ String paramName = entry.getKey();
+ Object paramValue = entry.getValue();
+
+ // Check for multivalued parameter
+ if (paramValue instanceof String[]) {
+ String[] paramValues = (String[]) paramValue;
+ for (int j = 0; j < paramValues.length; j++) {
+ buffer.append(paramName);
+ buffer.append("=");
+ buffer.append(ClickUtils.encodeUrl(paramValues[j], context));
+ if (j < paramValues.length - 1) {
+ buffer.append("&");
+ }
+ }
+ } else {
+ if (paramValue != null) {
+ buffer.append(paramName);
+ buffer.append("=");
+ buffer.append(ClickUtils.encodeUrl(paramValue, context));
+ }
+ }
+ if (i.hasNext()) {
+ buffer.append("&");
+ }
+ }
+
+ if (buffer.length() > 0) {
+ if (location.contains("?")) {
+ location += "&" + buffer.toString();
+ } else {
+ location += "?" + buffer.toString();
+ }
+ }
+ }
+
+ redirect = location;
+ }
+
+ /**
+ * Set the request to redirect to the given page class and and append
+ * the map of request parameters to the page URL.
+ *
+ * The map keys will be used as the request parameter names and the map
+ * values will be used as the request parameter values.
+ *
+ * @see #setRedirect(java.lang.String, java.util.Map)
+ * @see #setRedirect(java.lang.String)
+ *
+ * @param pageClass the class of the Page to redirect the request to
+ * @param params the map of request parameter name and value pairs
+ * @throws IllegalArgumentException if the Page Class is not configured
+ * with a unique path
+ */
+ public void setRedirect(Class extends Page> pageClass,
+ Map params) {
+
+ String target = getContext().getPagePath(pageClass);
+
+ // If page class maps to a jsp, convert to htm which allows ClickServlet
+ // to process the redirect
+ if (target != null && target.endsWith(".jsp")) {
+ target = StringUtils.replaceOnce(target, ".jsp", ".htm");
+ }
+
+ setRedirect(target, params);
+ }
+
+ /**
+ * Return the path of the page border template to render, by default this
+ * method returns {@link #getPath()}.
+ *
+ * Pages can override this method to return an alternative border page
+ * template. This is very useful when implementing an standardized look and
+ * feel for a web site. The example below provides a BorderedPage base Page
+ * which other site templated Pages should extend.
+ *
+ *
+ * public class BorderedPage extends Page {
+ * public String getTemplate() {
+ * return"border.htm";
+ * }
+ * }
+ *
+ * The BorderedPage returns the page border template "border.htm":
+ *
+ *
+ *
+ * Other pages insert their content into this template, via their
+ * {@link #path} property using the Velocity
+ * #parse
+ * directive. Note the $path value is automatically
+ * added to the VelocityContext by the ClickServlet.
+ *
+ * @return the path of the page template to render, by default returns
+ * {@link #getPath()}
+ */
+ public String getTemplate() {
+ return template == null ? getPath() : template;
+ }
+
+ /**
+ * Set the page border template path.
+ *
+ * Note: if this value is not set, {@link #getTemplate()} will default
+ * to {@link #getPath()}.
+ *
+ * @param template the border template path
+ */
+ public void setTemplate(String template) {
+ this.template = template;
+ }
+
+ // Private methods --------------------------------------------------------
+
+ /**
+ * Replace the current control with the new control.
+ *
+ * @param currentControl the control currently contained in the page
+ * @param newControl the control to replace the current control container in
+ * the page
+ *
+ * @throws IllegalStateException if the currentControl is not contained in
+ * the page
+ */
+ private void replaceControl(Control currentControl, Control newControl) {
+
+ // Current control and new control are referencing the same object
+ // so we exit early
+ if (currentControl == newControl) {
+ return;
+ }
+
+ int controlIndex = getControls().indexOf(currentControl);
+ if (controlIndex == -1) {
+ throw new IllegalStateException("Cannot replace the given control"
+ + " because it is not present in the page");
+ }
+
+ // Note: set parent first since setParent might veto further processing
+ newControl.setParent(this);
+ currentControl.setParent(null);
+
+ // Set control to current control index
+ getControls().set(controlIndex, newControl);
+
+ addModel(newControl.getName(), newControl);
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/PageInterceptor.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/PageInterceptor.java
new file mode 100644
index 0000000000..42b4829629
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/PageInterceptor.java
@@ -0,0 +1,238 @@
+/*
+ * 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.openidentityplatform.openam.click;
+
+/**
+ * Provides a Page life cycle interceptor. Classes implementing this interface
+ * can be used to listen for key page life cycle events and abort further page
+ * processing if required.
+ *
+ * PageInterceptors can be used for many different purposes including:
+ *
+ *
enforcing application wide page security policies
+ *
injecting dependencies into page objects
+ *
logging and profiling page performance
+ *
+ *
+ * A Click application can define multiple page interceptors that are invoked in
+ * the order in which they are returned by the ConfigService.
+ *
+ *
Scope
+ *
+ * Page interceptors can be defined with a request level scope, whereby a new
+ * page interceptor will be created with each page request providing a thread
+ * safe programming model.
+ *
+ * Please note, as new interceptor instances are created with each request, care
+ * should be taken to ensure that these objects are light weight and do not
+ * introduce a performance bottleneck into your application.
+ *
+ * Alternatively, page interceptors can be defined with application level scope
+ * whereby a single instance is created for the application and is used for
+ * all requests.
+ *
+ * Note application scope interceptors are more efficient that request scope
+ * interceptors, but you are responsible for ensuring that they are thread safe
+ * and support reentrant method invocations as multiple page requests are
+ * processed at the same time.
+ *
+ *
Configuration
+ *
+ * Application PageInterceptors are configured in the click.xml
+ * configuration file. PageInterceptors must support construction using a
+ * no-args public constructor.
+ *
+ * Page interceptors can have multiple properties configured with their XML
+ * definition which are set after the constructor has been called. Properties
+ * are set using OGNL via {@link org.apache.click.util.PropertyUtils}.
+ *
+ * An example configuration is provided below:
+ *
+ *
+ *
+ * The default scope for page interceptors is "request", but this can be configured
+ * as "application" as is done in the example configuration above.
+ *
+ *
+ */
+public interface PageInterceptor {
+
+ /**
+ * Provides a before page object creation interceptor method, which is passed
+ * the class of the page to be instantiated and the page request context.
+ * If this method returns true then the normal page processing is performed,
+ * otherwise if this method returns false the page instance is never created
+ * and the request is considered to have been handled.
+ *
+ * @param pageClass the class of the page to be instantiated
+ * @param context the page request context
+ * @return true to continue normal page processing or false whereby the
+ * request is considered to be handled
+ */
+ public boolean preCreate(Class extends Page> pageClass, Context context);
+
+ /**
+ * Provides a post page object creation interceptor method, which is passed
+ * the instance of the newly created page. This interceptor method is called
+ * before the page {@link Page#onSecurityCheck()} method is invoked.
+ *
+ * If this method returns true then the normal page processing is performed,
+ * otherwise if this method returns false the request is considered to have
+ * been handled.
+ *
+ * Please note the page {@link Page#onDestroy()} method will still be invoked.
+ *
+ * @param page the newly instantiated page instance
+ * @return true to continue normal page processing or false whereby the
+ * request is considered to be handled
+ */
+ public boolean postCreate(Page page);
+
+ /**
+ * Provides a page interceptor before response method. This method is invoked
+ * prior to the page redirect, forward or rendering phase.
+ *
+ * If this method returns true then the normal page processing is performed,
+ * otherwise if this method returns false request is considered to have been
+ * handled.
+ *
+ * Please note the page {@link Page#onDestroy()} method will still be invoked.
+ *
+ * @param page the newly instantiated page instance
+ * @return true to continue normal page processing or false whereby the
+ * request is considered to be handled
+ */
+ public boolean preResponse(Page page);
+
+ /**
+ * Provides a post page destroy interceptor method. This interceptor method
+ * is called immediately after the page {@link Page#onDestroy()} method is
+ * invoked.
+ *
+ * @param page the page object which has just been destroyed
+ */
+ public void postDestroy(Page page);
+
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/ajax/AjaxBehavior.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/ajax/AjaxBehavior.java
new file mode 100644
index 0000000000..fac9f69b4e
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/ajax/AjaxBehavior.java
@@ -0,0 +1,108 @@
+/*
+ * 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.openidentityplatform.openam.click.ajax;
+
+
+import org.openidentityplatform.openam.click.ActionResult;
+import org.openidentityplatform.openam.click.Behavior;
+import org.openidentityplatform.openam.click.Context;
+import org.openidentityplatform.openam.click.Control;
+
+/**
+ * AjaxBehavior extends the basic Behavior functionality to allow Controls to
+ * handle and process incoming Ajax requests.
+ *
+ * To handle an Ajax request, AjaxBehavior exposes the listener method:
+ * {@link #onAction(org.openidentityplatform.openam.click.Control) onAction}.
+ * The onAction method returns an ActionResult that is rendered back
+ * to the browser.
+ *
+ * Before Click invokes the onAction method it checks whether the request
+ * is targeted at the AjaxBehavior by invoking the method
+ * {@link #isAjaxTarget(org.openidentityplatform.openam.click.Context) Behavior.isAjaxTarget()}.
+ * Click will only invoke onAction if isAjaxTarget returns true.
+ */
+public interface AjaxBehavior extends Behavior {
+
+ /**
+ * This method can be implemented to handle and respond to an Ajax request.
+ * For example:
+ *
+ *
+ * public void onInit() {
+ * ActionLink link = new ActionLink("link");
+ * link.addBehaior(new DefaultAjaxBehavior() {
+ *
+ * public ActionResult onAction(Control source) {
+ * ActionResult result = new ActionResult("<h1>Hello world</h1>", ActionResult.HTML);
+ * return result;
+ * }
+ * });
+ * }
+ *
+ * @param source the control the behavior is attached to
+ * @return the action result instance
+ */
+ public ActionResult onAction(Control source);
+
+ /**
+ * Return true if the behavior is the request target, false otherwise.
+ *
+ * This method is queried by Click to determine if the behavior's
+ * {@link #onAction(org.openidentityplatform.openam.click.Control)} method should be called in
+ * response to a request.
+ *
+ * By exposing this method through the Behavior interface it provides
+ * implementers with fine grained control over whether the Behavior's
+ * {@link #onAction(org.openidentityplatform.openam.click.Control)} method should be invoked or not.
+ *
+ * Below is an example implementation:
+ *
+ *
+ * public CustomBehavior implements Behavior {
+ *
+ * private String eventType;
+ *
+ * public CustomBehavior(String eventType) {
+ * // The event type of the behavior
+ * super(eventType);
+ * }
+ *
+ * public boolean isAjaxTarget(Context context) {
+ * // Retrieve the eventType parameter from the incoming request
+ * String eventType = context.getRequestParameter("type");
+ *
+ * // Check if this Behavior's eventType matches the request
+ * // "type" parameter
+ * return StringUtils.equalsIgnoreCase(this.eventType, eventType);
+ * }
+ *
+ * public ActionResult onAction(Control source) {
+ * // If isAjaxTarget returned true, the onAction method will be
+ * // invoked
+ * ...
+ * }
+ * }
+ *
+ * @param context the request context
+ * @return true if the behavior is the request target, false otherwise
+ */
+ public boolean isAjaxTarget(Context context);
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractContainer.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractContainer.java
new file mode 100644
index 0000000000..fa553be154
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractContainer.java
@@ -0,0 +1,409 @@
+/*
+ * 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.openidentityplatform.openam.click.control;
+
+import org.openidentityplatform.openam.click.Control;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.openidentityplatform.openam.click.util.ContainerUtils;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides a default implementation of the {@link org.openidentityplatform.openam.click.control.Container} interface
+ * to make it easier for developers to create their own containers.
+ *
+ * Subclasses can override {@link #getTag()} to return a specific HTML element.
+ *
+ * The following example shows how to create an HTML div element:
+ *
+ *
+ * public class Div extends AbstractContainer {
+ *
+ * public String getTag() {
+ * // Return the HTML tag
+ * return "div";
+ * }
+ * }
+ */
+public abstract class AbstractContainer extends AbstractControl implements
+ Container {
+
+ // Constants --------------------------------------------------------------
+
+ private static final long serialVersionUID = 1L;
+
+ // Instance Variables -----------------------------------------------------
+
+ /** The list of controls. */
+ protected List controls;
+
+ /** The map of controls keyed by field name. */
+ protected Map controlMap;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * Create a container with no name defined.
+ */
+ public AbstractContainer() {
+ }
+
+ /**
+ * Create a container with the given name.
+ *
+ * @param name the container name
+ */
+ public AbstractContainer(String name) {
+ super(name);
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * @see org.openidentityplatform.openam.click.control.Container#add(Control).
+ *
+ * Please note: if the container contains a control with the same name
+ * as the given control, that control will be
+ * {@link #replace(Control, Control) replaced}
+ * by the given control. If a control has no name defined it cannot be replaced.
+ *
+ * @param control the control to add to the container
+ * @return the control that was added to the container
+ * @throws IllegalArgumentException if the control is null
+ */
+ public Control add(Control control) {
+ return insert(control, getControls().size());
+ }
+
+ /**
+ * Add the control to the container at the specified index, and return the
+ * added instance.
+ *
+ * Please note: if the container contains a control with the same name
+ * as the given control, that control will be
+ * {@link #replace(Control, Control) replaced}
+ * by the given control. If a control has no name defined it cannot be replaced.
+ *
+ * Also note if the specified control already has a parent assigned,
+ * it will automatically be removed from that parent and inserted into this
+ * container.
+ *
+ * @see org.openidentityplatform.openam.click.control.Container#insert(Control, int)
+ *
+ * @param control the control to add to the container
+ * @param index the index at which the control is to be inserted
+ * @return the control that was added to the container
+ *
+ * @throws IllegalArgumentException if the control is null or if the control
+ * and container is the same instance
+ *
+ * @throws IndexOutOfBoundsException if index is out of range
+ * (index < 0 || index > getControls().size())
+ */
+ public Control insert(Control control, int index) {
+ // Check if panel already contains the control
+ String controlName = control.getName();
+ if (controlName != null) {
+ // Check if container already contains the control
+ Control currentControl = getControlMap().get(control.getName());
+
+ // If container already contains the control do a replace
+ if (currentControl != null) {
+
+ // Current control and new control are referencing the same object
+ // so we exit early
+ if (currentControl == control) {
+ return control;
+ }
+
+ // If the two controls are different objects, we remove the current
+ // control and add the given control
+ return replace(currentControl, control);
+ }
+ }
+
+ return ContainerUtils.insert(this, control, index, getControlMap());
+ }
+
+ /**
+ * @seeorg.openidentityplatform.openam.click.control.Container#remove(Control).
+ *
+ * @param control the control to remove from the container
+ * @return true if the control was removed from the container
+ * @throws IllegalArgumentException if the control is null
+ */
+ public boolean remove(Control control) {
+ return ContainerUtils.remove(this, control, getControlMap());
+ }
+
+ /**
+ * Replace the control in the container at the specified index, and return
+ * the newly added control.
+ *
+ * @see org.openidentityplatform.openam.click.control.Container#replace(Control, Control)
+ *
+ * @param currentControl the control currently contained in the container
+ * @param newControl the control to replace the current control contained in
+ * the container
+ * @return the new control that replaced the current control
+ *
+ * @deprecated this method was used for stateful pages, which have been deprecated
+ *
+ * @throws IllegalArgumentException if the currentControl or newControl is
+ * null
+ * @throws IllegalStateException if the currentControl is not contained in
+ * the container
+ */
+ public Control replace(Control currentControl, Control newControl) {
+ int controlIndex = getControls().indexOf(currentControl);
+ return ContainerUtils.replace(this, currentControl, newControl,
+ controlIndex, getControlMap());
+ }
+
+ /**
+ * @see org.apache.click.control.Container#getControls().
+ *
+ * @return the sequential list of controls held by the container
+ */
+ public List getControls() {
+ if (controls == null) {
+ controls = new ArrayList();
+ }
+ return controls;
+ }
+
+ /**
+ * @see org.apache.click.control.Container#getControl(String)
+ *
+ * @param controlName the name of the control to get from the container
+ * @return the named control from the container if found or null otherwise
+ */
+ public Control getControl(String controlName) {
+ if (hasControls()) {
+ return getControlMap().get(controlName);
+ }
+ return null;
+ }
+
+ /**
+ * @see Container#contains(Control)
+ *
+ * @param control the control whose presence in this container is to be tested
+ * @return true if the container contains the specified control
+ */
+ public boolean contains(Control control) {
+ return getControls().contains(control);
+ }
+
+ /**
+ * Returns true if this container has existing controls, false otherwise.
+ *
+ * @return true if the container has existing controls, false otherwise.
+ */
+ public boolean hasControls() {
+ return (controls != null) && !controls.isEmpty();
+ }
+
+ /**
+ * Return the map of controls where each map's key / value pair will consist
+ * of the control name and instance.
+ *
+ * Controls added to the container that did not specify a {@link #name},
+ * will not be included in the returned map.
+ *
+ * @return the map of controls
+ */
+ public Map getControlMap() {
+ if (controlMap == null) {
+ controlMap = new HashMap();
+ }
+ return controlMap;
+ }
+
+ /**
+ * @see org.openidentityplatform.openam.click.control.AbstractControl#getControlSizeEst().
+ *
+ * @return the estimated rendered control size in characters
+ */
+ @Override
+ public int getControlSizeEst() {
+ int size = 20;
+
+ if (getTag() != null && hasAttributes()) {
+ size += 20 * getAttributes().size();
+ }
+
+ if (hasControls()) {
+ size += getControls().size() * size;
+ }
+
+ return size;
+ }
+
+ /**
+ * @see Control#onProcess().
+ *
+ * @return true to continue Page event processing or false otherwise
+ */
+ @Override
+ public boolean onProcess() {
+
+ boolean continueProcessing = true;
+
+ for (Control control : getControls()) {
+ if (!control.onProcess()) {
+ continueProcessing = false;
+ }
+ }
+
+ dispatchActionEvent();
+
+ return continueProcessing;
+ }
+
+ /**
+ * @see Control#onDestroy()
+ */
+ @Override
+ public void onDestroy() {
+ for (Control control : getControls()) {
+ try {
+ control.onDestroy();
+ } catch (Throwable t) {
+ ClickUtils.getLogService().error("onDestroy error", t);
+ }
+ }
+ }
+
+ /**
+ * @see Control#onInit()
+ */
+ @Override
+ public void onInit() {
+ super.onInit();
+ for (Control control : getControls()) {
+ control.onInit();
+ }
+ }
+
+ /**
+ * @see Control#onRender()
+ */
+ @Override
+ public void onRender() {
+ for (Control control : getControls()) {
+ control.onRender();
+ }
+ }
+
+ /**
+ * Render the HTML representation of the container and all its child
+ * controls to the specified buffer.
+ *
+ * If {@link #getTag()} returns null, this method will render only its
+ * child controls.
+ *
+ * @see org.openidentityplatform.openam.click.control.AbstractControl#render(HtmlStringBuffer)
+ *
+ * @param buffer the specified buffer to render the control's output to
+ */
+ @Override
+ public void render(HtmlStringBuffer buffer) {
+
+ //If tag is set, render it
+ if (getTag() != null) {
+ renderTagBegin(getTag(), buffer);
+ buffer.closeTag();
+ if (hasControls()) {
+ buffer.append("\n");
+ }
+ renderContent(buffer);
+ renderTagEnd(getTag(), buffer);
+
+ } else {
+
+ //render only content because no tag is specified
+ renderContent(buffer);
+ }
+ }
+
+ /**
+ * Returns the HTML representation of this control.
+ *
+ * This method delegates the rendering to the method
+ * {@link #render(HtmlStringBuffer)}. The size of buffer
+ * is determined by {@link #getControlSizeEst()}.
+ *
+ * @see Object#toString()
+ *
+ * @return the HTML representation of this control
+ */
+ @Override
+ public String toString() {
+ HtmlStringBuffer buffer = new HtmlStringBuffer(getControlSizeEst());
+ render(buffer);
+ return buffer.toString();
+ }
+
+ // Protected Methods ------------------------------------------------------
+
+ /**
+ * @see AbstractControl#renderTagEnd(String, HtmlStringBuffer).
+ *
+ * @param tagName the name of the tag to close
+ * @param buffer the buffer to append the output to
+ */
+ @Override
+ protected void renderTagEnd(String tagName, HtmlStringBuffer buffer) {
+ buffer.elementEnd(tagName);
+ }
+
+ /**
+ * Render this container content to the specified buffer.
+ *
+ * @param buffer the buffer to append the output to
+ */
+ protected void renderContent(HtmlStringBuffer buffer) {
+ renderChildren(buffer);
+ }
+
+ /**
+ * Render this container children to the specified buffer.
+ *
+ * @see #getControls()
+ *
+ * @param buffer the buffer to append the output to
+ */
+ protected void renderChildren(HtmlStringBuffer buffer) {
+ for (Control control : getControls()) {
+
+ int before = buffer.length();
+ control.render(buffer);
+
+ int after = buffer.length();
+ if (before != after) {
+ buffer.append("\n");
+ }
+ }
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractControl.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractControl.java
new file mode 100644
index 0000000000..a651f26699
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractControl.java
@@ -0,0 +1,1121 @@
+package org.openidentityplatform.openam.click.control;
+
+/*
+ * 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.
+ */
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.Map.Entry;
+
+import jakarta.servlet.ServletContext;
+
+import org.openidentityplatform.openam.click.ActionEventDispatcher;
+import org.openidentityplatform.openam.click.ActionListener;
+import org.openidentityplatform.openam.click.Behavior;
+import org.openidentityplatform.openam.click.ControlRegistry;
+import org.openidentityplatform.openam.click.Context;
+import org.openidentityplatform.openam.click.Control;
+import org.openidentityplatform.openam.click.Page;
+import org.openidentityplatform.openam.click.element.Element;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+
+/**
+ * Provides a default implementation of the {@link Control} interface
+ * to make it easier for developers to create their own controls.
+ *
+ * Subclasses are expected to at least override {@link #getTag()}
+ * to differentiate the control. However some controls do not map cleanly
+ * to an html tag, in which case you can override
+ * {@link #render(org.openidentityplatform.openam.click.util.HtmlStringBuffer)} for complete control
+ * over the output.
+ *
+ * Below is an example of creating a new control called MyField:
+ *
+ * public class MyField extends AbstractControl {
+ *
+ * private String value;
+ *
+ * public void setValue(String value) {
+ * this.value = value;
+ * }
+ *
+ * public String getValue() {
+ * return value;
+ * }
+ *
+ * public String getTag() {
+ * // Return the HTML tag
+ * return "input";
+ * }
+ *
+ * public boolean onProcess() {
+ * // Bind the request parameter to the field value
+ * String requestValue = getContext().getRequestParameter(getName());
+ * setValue(requestValue);
+ *
+ * // Invoke any listener of MyField
+ * return dispatchActionEvent();
+ * }
+ * }
+ *
+ * By overriding {@link #getTag()} one can specify the html tag to render.
+ *
+ * Overriding {@link #onProcess()} allows one to bind the servlet request
+ * parameter to MyField value. The {@link #dispatchActionEvent()} method
+ * registers the listener for this control on the Context. Once the onProcess
+ * event has finished, all registered listeners will be fired.
+ *
+ * To view the html rendered by MyField invoke the control's {@link #toString()}
+ * method:
+ *
+ *
+ * public class Test {
+ * public static void main (String args[]) {
+ * // Create mock context in which to test the control.
+ * MockContext.initContext();
+ *
+ * String fieldName = "myfield";
+ * MyField myfield = new MyField(fieldName);
+ * String output = myfield.toString();
+ * System.out.println(output);
+ * }
+ * }
+ *
+ * Executing the above test results in the following output:
+ *
+ *
+ * <input name="myfield" id="myfield"/>
+ *
+ * Also see {@link org.apache.click.Control} javadoc for an explanation of the
+ * Control execution sequence.
+ *
+ *
Message Resources and Internationalization (i18n)
+ *
+ * Controls support a hierarchy of resource bundles for displaying messages.
+ * These localized messages can be accessed through the methods:
+ *
+ *
+ *
{@link #getMessage(String)}
+ *
{@link #getMessage(String, Object...)}
+ *
{@link #getMessages()}
+ *
+ *
+ * The order in which localized messages resolve are described in the
+ * user guide.
+ */
+public abstract class AbstractControl implements Control {
+
+ // Constants --------------------------------------------------------------
+
+ private static final long serialVersionUID = 1L;
+
+ // Instance Variables -----------------------------------------------------
+
+ /** The control's action listener. */
+ protected ActionListener actionListener;
+
+ /** The control's list of {@link org.apache.click.Behavior behaviors}. */
+ protected Set behaviors;
+
+ /**
+ * The list of page HTML HEAD elements including: Javascript imports,
+ * Css imports, inline Javascript and inline Css.
+ */
+ protected List headElements;
+
+ /** The Control attributes Map. */
+ protected Map attributes;
+
+ /** The Control localized messages Map. */
+ protected transient Map messages;
+
+ /** The Control name. */
+ protected String name;
+
+ /** The control's parent. */
+ protected Object parent;
+
+ /**
+ * The Map of CSS style attributes.
+ *
+ * @deprecated use {@link #addStyleClass(String)} and
+ * {@link #removeStyleClass(String)} instead.
+ */
+ protected Map styles;
+
+ /** The listener target object. */
+ protected Object listener;
+
+ /** The listener method name. */
+ protected String listenerMethod;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * Create a control with no name defined.
+ */
+ public AbstractControl() {
+ }
+
+ /**
+ * Create a control with the given name.
+ *
+ * @param name the control name
+ */
+ public AbstractControl(String name) {
+ if (name != null) {
+ setName(name);
+ }
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * Returns the controls html tag.
+ *
+ * Subclasses should override this method and return the correct tag.
+ *
+ * This method returns null by default.
+ *
+ * Example tags include table, form, a and
+ * input.
+ *
+ * @return this controls html tag
+ */
+ public String getTag() {
+ return null;
+ }
+
+ /**
+ * Return the control's action listener. If the control has a listener
+ * target and listener method defined, this method will return an
+ * {@link org.apache.click.ActionListener} instance.
+ *
+ * @return the control's action listener
+ */
+ public ActionListener getActionListener() {
+ if (actionListener == null && listener != null && listenerMethod != null) {
+ actionListener = new ActionListener() {
+
+ private static final long serialVersionUID = 1L;
+
+ public boolean onAction(Control source) {
+ return ClickUtils.invokeListener(listener, listenerMethod);
+ }
+ };
+ }
+ return actionListener;
+ }
+
+ /**
+ * Set the control's action listener.
+ *
+ * @param listener the control's action listener
+ */
+ public void setActionListener(ActionListener listener) {
+ this.actionListener = listener;
+ }
+
+ /**
+ * Returns true if this control has any
+ * Behaviors registered, false otherwise.
+ *
+ * @return true if this control has any Behaviors registered,
+ * false otherwise
+ */
+ public boolean hasBehaviors() {
+ return (behaviors != null && !behaviors.isEmpty());
+ }
+
+ /**
+ * Add the given Behavior to the control's Set of
+ * {@link #getBehaviors() Behaviors}.
+ *
+ * In addition, the Control will be registered with the
+ * {@link org.apache.click.ControlRegistry#registerAjaxTarget(org.apache.click.Control) ControlRegistry}
+ * as a potential Ajax target control and to have it's
+ * Behaviors processed by the Click runtime.
+ *
+ * @param behavior the Behavior to add
+ */
+ public void addBehavior(Behavior behavior) {
+ if (behavior == null) {
+ throw new IllegalArgumentException("Behavior cannot be null");
+ }
+
+ // Ensure we register the behavior only once
+ if (!hasBehaviors()) {
+ ControlRegistry.registerAjaxTarget(this);
+ }
+
+ getBehaviors().add(behavior);
+ }
+
+ /**
+ * Remove the given Behavior from the Control's Set of
+ * {@link #getBehaviors() Behaviors}.
+ *
+ * @param behavior the Behavior to remove
+ */
+ public void removeBehavior(Behavior behavior) {
+ getBehaviors().remove(behavior);
+ }
+
+ /**
+ * Returns the Set of Behaviors for this control.
+ *
+ * @return the Set of Behaviors for this control
+ */
+ public Set getBehaviors() {
+ if (behaviors == null) {
+ behaviors = new HashSet();
+ }
+ return behaviors;
+ }
+
+ /**
+ * Returns true if this control is an AJAX target, false
+ * otherwise.
+ *
+ * The control is defined as an Ajax target if the control {@link #getId() ID}
+ * is send as a request parameter.
+ *
+ * @param context the request context
+ * @return true if this control is an AJAX target, false
+ * otherwise
+ */
+ public boolean isAjaxTarget(Context context) {
+ // TODO each control could have an optimized version of isAjaxTarget
+ // targeting specifically that control. For now we just check that the
+ // control id is present. Not all controls can use an ID for example:
+ // ActionLink
+ String id = getId();
+ if (id != null) {
+ return context.hasRequestParameter(id);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Return the control HTML attribute with the given name, or null if the
+ * attribute does not exist.
+ *
+ * @param name the name of link HTML attribute
+ * @return the link HTML attribute
+ */
+ public String getAttribute(String name) {
+ if (hasAttributes()) {
+ return getAttributes().get(name);
+ }
+ return null;
+ }
+
+ /**
+ * Set the control attribute with the given attribute name and value. You would
+ * generally use attributes if you were creating the entire Control
+ * programmatically and rendering it with the {@link #toString()} method.
+ *
+ * For example given the ActionLink:
+ *
+ *
+ *
+ * Note: for style and class attributes you can
+ * also use the methods {@link #setStyle(String, String)} and
+ * {@link #addStyleClass(String)}.
+ *
+ * @see #setStyle(String, String)
+ * @see #addStyleClass(String)
+ * @see #removeStyleClass(String)
+ *
+ * @param name the attribute name
+ * @param value the attribute value
+ * @throws IllegalArgumentException if name parameter is null
+ */
+ public void setAttribute(String name, String value) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+
+ if (value != null) {
+ getAttributes().put(name, value);
+ } else {
+ getAttributes().remove(name);
+ }
+ }
+
+ /**
+ * Return the control's attributes Map.
+ *
+ * @return the control's attributes Map.
+ */
+ public Map getAttributes() {
+ if (attributes == null) {
+ attributes = new HashMap();
+ }
+ return attributes;
+ }
+
+ /**
+ * Return true if the control has attributes or false otherwise.
+ *
+ * @return true if the control has attributes on false otherwise
+ */
+ public boolean hasAttributes() {
+ return attributes != null && !attributes.isEmpty();
+ }
+
+ /**
+ * Returns true if specified attribute is defined, false otherwise.
+ *
+ * @param name the specified attribute to check
+ * @return true if name is a defined attribute
+ */
+ public boolean hasAttribute(String name) {
+ return hasAttributes() && getAttributes().containsKey(name);
+ }
+
+ /**
+ * @see org.apache.click.Control#getContext()
+ *
+ * @return the Page request Context
+ */
+ public Context getContext() {
+ return Context.getThreadLocalContext();
+ }
+
+ /**
+ * @see Control#getName()
+ *
+ * @return the name of the control
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @see Control#setName(String)
+ *
+ * @param name of the control
+ * @throws IllegalArgumentException if the name is null
+ */
+ public void setName(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+ this.name = name;
+ }
+
+ /**
+ * Return the "id" attribute value if defined, or the control name otherwise.
+ *
+ * @see org.apache.click.Control#getId()
+ *
+ * @return HTML element identifier attribute "id" value
+ */
+ public String getId() {
+ String id = getAttribute("id");
+
+ return (id != null) ? id : getName();
+ }
+
+ /**
+ * Set the HTML id attribute for the control with the given value.
+ *
+ * @param id the element HTML id attribute value to set
+ */
+ public void setId(String id) {
+ if (id != null) {
+ setAttribute("id", id);
+ } else {
+ getAttributes().remove("id");
+ }
+ }
+
+ /**
+ * Return the localized message for the given key or null if not found.
+ * The resource message returned will use the Locale obtained from the
+ * Context.
+ *
+ * This method will attempt to lookup the localized message in the
+ * parent's messages, which resolves to the Page's resource bundle.
+ *
+ * If the message was not found, this method will attempt to look up the
+ * value in the /click-control.properties message properties file,
+ * through the method {@link #getMessages()}.
+ *
+ * If still not found, this method will return null.
+ *
+ * @param name the name of the message resource
+ * @return the named localized message for the control, or null if not found
+ */
+ public String getMessage(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+
+ String message = null;
+
+ message = ClickUtils.getParentMessage(this, name);
+
+ if (message == null && getMessages().containsKey(name)) {
+ message = getMessages().get(name);
+ }
+
+ return message;
+ }
+
+ /**
+ * Return the formatted message for the given resource name and message
+ * format arguments or null if no message was found. The resource
+ * message returned will use the Locale obtained from the Context.
+ *
+ * {@link #getMessage(java.lang.String)} is invoked to retrieve the message
+ * for the specified name.
+ *
+ * @param name resource name of the message
+ * @param args the message arguments to format
+ * @return the named localized message for the control or null if no message
+ * was found
+ */
+ public String getMessage(String name, Object... args) {
+ String value = getMessage(name);
+ if (value == null) {
+ return null;
+ }
+ return MessageFormat.format(value, args);
+ }
+
+ /**
+ * Return a Map of localized messages for the control. The messages returned
+ * will use the Locale obtained from the Context.
+ *
+ * @return a Map of localized messages for the control
+ * @throws IllegalStateException if the context for the control has not be set
+ */
+ public Map getMessages() {
+ if (messages == null) {
+ messages = getContext().createMessagesMap(getClass(), CONTROL_MESSAGES);
+ }
+ return messages;
+ }
+
+ /**
+ * @see org.apache.click.Control#getParent()
+ *
+ * @return the Control's parent
+ */
+ public Object getParent() {
+ return parent;
+ }
+
+ /**
+ * @see org.apache.click.Control#setParent(Object)
+ *
+ * @param parent the parent of the Control
+ * @throws IllegalArgumentException if the given parent instance is
+ * referencing this object: if (parent == this)
+ */
+ public void setParent(Object parent) {
+ if (parent == this) {
+ throw new IllegalArgumentException("Cannot set parent to itself");
+ }
+ this.parent = parent;
+ }
+
+ /**
+ * @see org.apache.click.Control#onProcess()
+ *
+ * @return true to continue Page event processing or false otherwise
+ */
+ public boolean onProcess() {
+ dispatchActionEvent();
+ return true;
+ }
+
+ /**
+ * Set the controls event listener.
+ *
+ * The method signature of the listener is:
+ *
must have a valid Java method name
+ *
takes no arguments
+ *
returns a boolean value
+ *
+ *
+ * An example event listener method would be:
+ *
+ *
+ *
+ * @param listener the listener object with the named method to invoke
+ * @param method the name of the method to invoke
+ */
+ public void setListener(Object listener, String method) {
+ this.listener = listener;
+ this.listenerMethod = method;
+ }
+
+ /**
+ * This method does nothing. Subclasses may override this method to perform
+ * initialization.
+ *
+ * @see org.apache.click.Control#onInit()
+ */
+ public void onInit() {
+ }
+
+ /**
+ * This method does nothing. Subclasses may override this method to perform
+ * clean up any resources.
+ *
+ * @see org.apache.click.Control#onDestroy()
+ */
+ public void onDestroy() {
+ }
+
+ /**
+ * This method does nothing. Subclasses may override this method to deploy
+ * static web resources.
+ *
+ * @see org.openidentityplatform.openam.click.Control#onDeploy(ServletContext)
+ *
+ * @param servletContext the servlet context
+ */
+ public void onDeploy(ServletContext servletContext) {
+ }
+
+ /**
+ * This method does nothing. Subclasses may override this method to perform
+ * pre rendering logic.
+ *
+ * @see org.apache.click.Control#onRender()
+ */
+ public void onRender() {
+ }
+
+ /**
+ * @deprecated use the new {@link #getHeadElements()} instead
+ *
+ * @return the HTML includes statements for the control stylesheet and
+ * JavaScript files
+ */
+ public final String getHtmlImports() {
+ throw new UnsupportedOperationException("Use getHeadElements instead");
+ }
+
+ /**
+ * @see org.apache.click.Control#getHeadElements()
+ *
+ * @return the list of HEAD elements to be included in the page
+ */
+ public List getHeadElements() {
+ if (headElements == null) {
+ // Most controls won't provide their own head elements, so save
+ // memory by creating an empty array list
+ headElements = new ArrayList(0);
+ }
+ return headElements;
+ }
+
+ /**
+ * Return the parent page of this control, or null if not defined.
+ *
+ * @return the parent page of this control, or null if not defined
+ */
+ public Page getPage() {
+ return ClickUtils.getParentPage(this);
+ }
+
+ /**
+ * Return the control CSS style for the given name.
+ *
+ * @param name the CSS style name
+ * @return the CSS style for the given name
+ */
+ public String getStyle(String name) {
+ if (hasAttribute("style")) {
+ String currentStyles = getAttribute("style");
+ Map stylesMap = parseStyles(currentStyles);
+ return stylesMap.get(name);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set the control CSS style name and value pair.
+ *
+ * For example given the ActionLink:
+ *
+ *
+ * To remove an existing style, set the value to null.
+ *
+ * @param name the CSS style name
+ * @param value the CSS style value
+ */
+ public void setStyle(String name, String value) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+
+ String oldStyles = getAttribute("style");
+
+ if (oldStyles == null) {
+
+ if (value == null) {
+ //Exit early
+ return;
+ } else {
+ //If value is not null, append the new style and return
+ getAttributes().put("style", name + ":" + value + ";");
+ return;
+ }
+ }
+
+ //Create buffer and estimate the new size
+ HtmlStringBuffer buffer = new HtmlStringBuffer(
+ oldStyles.length() + 10);
+
+ //Parse the current styles into a map
+ Map stylesMap = parseStyles(oldStyles);
+
+ //Check if the new style is already present
+ if (stylesMap.containsKey(name)) {
+
+ //If the style is present and the value is null, remove the style,
+ //otherwise replace it with the new value
+ if (value == null) {
+ stylesMap.remove(name);
+ } else {
+ stylesMap.put(name, value);
+ }
+ } else {
+ stylesMap.put(name, value);
+ }
+
+ //The styles map might be empty if the last style was removed
+ if (stylesMap.isEmpty()) {
+ getAttributes().remove("style");
+ return;
+ }
+
+ //Iterate over the stylesMap appending each entry to buffer
+ for (Entry entry : stylesMap.entrySet()) {
+ String styleName = entry.getKey();
+ String styleValue = entry.getValue();
+ buffer.append(styleName);
+ buffer.append(":");
+ buffer.append(styleValue);
+ buffer.append(";");
+ }
+ getAttributes().put("style", buffer.toString());
+ }
+
+ /**
+ * Return true if CSS styles are defined.
+ *
+ * @deprecated use {@link #hasAttribute(String)} instead
+ *
+ * @return true if CSS styles are defined
+ */
+ public boolean hasStyles() {
+ return (styles != null && !styles.isEmpty());
+ }
+
+ /**
+ * Return the Map of control CSS styles.
+ *
+ * @deprecated use {@link #getAttribute(String)} instead
+ *
+ * @return the Map of control CSS styles
+ */
+ public Map getStyles() {
+ if (styles == null) {
+ styles = new HashMap();
+ }
+ return styles;
+ }
+
+ /**
+ * Add the CSS class attribute. Null values will be ignored.
+ *
+ * For example given the ActionLink:
+ *
+ *
+ *
+ * @param value the class attribute to add
+ */
+ public void addStyleClass(String value) {
+ //If value is null, exit early
+ if (value == null) {
+ return;
+ }
+
+ //If any class attributes already exist, check if the specified class
+ //exists in the current set of classes.
+ if (hasAttribute("class")) {
+ String oldStyleClasses = getAttribute("class").trim();
+
+ //Check if the specified class exists in the class attribute set
+ boolean classExists = classExists(value, oldStyleClasses);
+
+ if (classExists) {
+ //If the class already exist, exit early
+ return;
+ }
+
+ //Specified class does not exist so add it with the other class
+ //attributes
+ getAttributes().put("class", oldStyleClasses + " " + value);
+
+ } else {
+ //Since no class attributes exist yet, only add the specified class
+ setAttribute("class", value);
+ }
+ }
+
+ /**
+ * Removes the CSS class attribute.
+ *
+ * @param value the CSS class attribute
+ */
+ public void removeStyleClass(String value) {
+ // If value is null, exit early
+ if (value == null) {
+ return;
+ }
+
+ // If any class attributes already exist, check if the specified class
+ // exists in the current set of classes.
+ if (hasAttribute("class")) {
+ String oldStyleClasses = getAttribute("class").trim();
+
+ //Check if the specified class exists in the class attribute set
+ boolean classExists = classExists(value, oldStyleClasses);
+
+ if (!classExists) {
+ //If the class does not exist, exit early
+ return;
+ }
+
+ // If the class does exist, parse the class attributes into a set
+ // and remove the specified class
+ Set styleClassSet = parseStyleClasses(oldStyleClasses);
+ styleClassSet.remove(value);
+
+ if (styleClassSet.isEmpty()) {
+ // If there are no more styleClasses left, remove the class
+ // attribute from the attributes list
+ getAttributes().remove("class");
+ } else {
+ // Otherwise render the styleClasses.
+ // Create buffer and estimate the new size
+ HtmlStringBuffer buffer = new HtmlStringBuffer(
+ oldStyleClasses.length() + value.length());
+
+ // Iterate over the styleClassSet appending each entry to buffer
+ Iterator it = styleClassSet.iterator();
+ while (it.hasNext()) {
+ String entry = it.next();
+ buffer.append(entry);
+ if (it.hasNext()) {
+ buffer.append(" ");
+ }
+ }
+ getAttributes().put("class", buffer.toString());
+ }
+ }
+ }
+
+ /**
+ * Render the control's output to the specified buffer.
+ *
+ * If {@link #getTag()} returns null, this method will return an empty
+ * string.
+ *
+ * @see org.apache.click.Control#render(org.apache.click.util.HtmlStringBuffer)
+ *
+ * @param buffer the specified buffer to render the control's output to
+ */
+ public void render(HtmlStringBuffer buffer) {
+ if (getTag() == null) {
+ return;
+ }
+ renderTagBegin(getTag(), buffer);
+ renderTagEnd(getTag(), buffer);
+ }
+
+ /**
+ * Returns the HTML representation of this control.
+ *
+ * This method delegates the rendering to the method
+ * {@link #render(org.openidentityplatform.openam.click.util.HtmlStringBuffer)}. The size of buffer
+ * is determined by {@link #getControlSizeEst()}.
+ *
+ * @see Object#toString()
+ *
+ * @return the HTML representation of this control
+ */
+ @Override
+ public String toString() {
+ if (getTag() == null) {
+ return "";
+ }
+ HtmlStringBuffer buffer = new HtmlStringBuffer(getControlSizeEst());
+ render(buffer);
+ return buffer.toString();
+ }
+
+ // Protected Methods ------------------------------------------------------
+
+ /**
+ * Dispatch an action event to the {@link org.apache.click.ActionEventDispatcher}.
+ *
+ * @see org.apache.click.ActionEventDispatcher#dispatchActionEvent(org.apache.click.Control, org.apache.click.ActionListener)
+ * @see org.apache.click.ActionEventDispatcher#dispatchAjaxBehaviors(org.apache.click.Control)
+ */
+ protected void dispatchActionEvent() {
+ if (getActionListener() != null) {
+ ActionEventDispatcher.dispatchActionEvent(this, getActionListener());
+ }
+
+ if (hasBehaviors()) {
+ ActionEventDispatcher.dispatchAjaxBehaviors(this);
+ }
+ }
+
+ /**
+ * Append all the controls attributes to the specified buffer.
+ *
+ * @param buffer the specified buffer to append all the attributes
+ */
+ protected void appendAttributes(HtmlStringBuffer buffer) {
+ if (hasAttributes()) {
+ buffer.appendAttributes(attributes);
+ }
+ }
+
+ /**
+ * Render the tag and common attributes including {@link #getId() id},
+ * class and style. The {@link #getName() name} attribute
+ * is not rendered by this control. It is up to subclasses
+ * whether to render the name attribute or not. Generally only
+ * {@link org.apache.click.control.Field} controls render the name attribute.
+ *
+ * Please note: the tag will not be closed by this method. This
+ * enables callers of this method to append extra attributes as needed.
+ *
+ * For example the following example:
+ *
+ *
+ * Field field = new TextField("mytext");
+ * HtmlStringBuffer buffer = new HtmlStringBuffer();
+ * field.renderTagBegin("input", buffer);
+ *
+ * will render:
+ *
+ *
+ * <input name="mytext" id="mytext"
+ *
+ * Note that the tag is not closed, so subclasses can render extra
+ * attributes.
+ *
+ * @param tagName the name of the tag to render
+ * @param buffer the buffer to append the output to
+ */
+ protected void renderTagBegin(String tagName,
+ HtmlStringBuffer buffer) {
+ if (tagName == null) {
+ throw new IllegalStateException("Tag cannot be null");
+ }
+
+ buffer.elementStart(tagName);
+
+ String id = getId();
+ if (id != null) {
+ buffer.appendAttribute("id", id);
+ }
+
+ appendAttributes(buffer);
+ }
+
+ /**
+ * Closes the specified tag.
+ *
+ * @param tagName the name of the tag to close
+ * @param buffer the buffer to append the output to
+ */
+ protected void renderTagEnd(String tagName, HtmlStringBuffer buffer) {
+ buffer.elementEnd();
+ }
+
+ /**
+ * Return the estimated rendered control size in characters.
+ *
+ * @return the estimated rendered control size in characters
+ */
+ protected int getControlSizeEst() {
+ int size = 0;
+ if (getTag() != null && hasAttributes()) {
+ //length of the markup -> > == 3
+ //1 * tag.length()
+ size += 3 + getTag().length();
+ //using 20 as an estimate
+ size += 20 * getAttributes().size();
+ }
+ return size;
+ }
+
+ // Private Methods --------------------------------------------------------
+
+ /**
+ * Parse the specified string of style attributes and return a Map
+ * of key/value pairs. Invalid key/value pairs will not be added to
+ * the map.
+ *
+ * @param style the string containing the styles to parse
+ * @return map containing key/value pairs of the specified style
+ * @throws IllegalArgumentException if style is null
+ */
+ private Map parseStyles(String style) {
+ if (style == null) {
+ throw new IllegalArgumentException("style cannot be null");
+ }
+
+ //LinkHashMap is used to keep the order of the style names. Probably
+ //makes no difference to browser but it makes testing easier since the
+ //order that styles are added are kept when rendering the control.
+ Map stylesMap = new LinkedHashMap();
+ StringTokenizer tokens = new StringTokenizer(style, ";");
+ while (tokens.hasMoreTokens()) {
+ String token = tokens.nextToken();
+ int keyEnd = token.indexOf(":");
+
+ //If there is no key/value delimiter or value is empty, continue
+ if (keyEnd == -1 || (keyEnd + 1) == token.length()) {
+ continue;
+ }
+ //Check that the value part of the key/value pair not only
+ //consists of a ';' char.
+ if (token.charAt(keyEnd + 1) == ';') {
+ continue;
+ }
+ String styleName = token.substring(0, keyEnd);
+ String styleValue = token.substring(keyEnd + 1);
+ stylesMap.put(styleName, styleValue);
+ }
+
+ return stylesMap;
+ }
+
+ /**
+ * Parse the specified string of style attributes and return a Map
+ * of key/value pairs. Invalid key/value pairs will not be added to
+ * the map.
+ *
+ * @param styleClasses the string containing the styles to parse
+ * @return map containing key/value pairs of the specified style
+ * @throws IllegalArgumentException if styleClasses is null
+ */
+ private Set parseStyleClasses(String styleClasses) {
+ if (styleClasses == null) {
+ throw new IllegalArgumentException("styleClasses cannot be null");
+ }
+
+ //LinkHashMap is used to keep the order of the class names. Probably
+ //makes no difference to browser but it makes testing easier since the
+ //order that classes were added in, are kept when rendering the control.
+ //Thus one can test whether the expected result and actual result match.
+ Set styleClassesSet = new LinkedHashSet();
+ StringTokenizer tokens = new StringTokenizer(styleClasses, " ");
+ while (tokens.hasMoreTokens()) {
+ String token = tokens.nextToken();
+ styleClassesSet.add(token);
+ }
+
+ return styleClassesSet;
+ }
+
+ /**
+ * Return true if the new value exists in the current value.
+ *
+ * @param newValue the value of the class attribute to check
+ * @param currentValue the current value to test
+ * @return true if the classToFind exists in the current value set
+ */
+ private boolean classExists(String newValue, String currentValue) {
+ //To find if a class eg. "myclass" exists, check the following:
+
+ //1. Check if "myclass" is the only value in the string
+ // -> "myclass"
+ if (newValue.length() == currentValue.length()
+ && currentValue.indexOf(newValue) == 0) {
+ return true;
+ }
+
+ //2. Check if "myclass" exists at beginning of string
+ // -> "myclass otherclass"
+ if (currentValue.startsWith(newValue + " ")) {
+ return true;
+ }
+
+ //3. Check if "myclass" exists in middle of string
+ // -> "anotherclass myclass otherclass"
+ if (currentValue.indexOf(" " + newValue + " ") >= 0) {
+ return true;
+ }
+
+ //4. Check if "myclass" exists at end of string
+ // -> "anotherclass myclass"
+ return (currentValue.endsWith(" " + newValue));
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractLink.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractLink.java
new file mode 100644
index 0000000000..e920367086
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/AbstractLink.java
@@ -0,0 +1,818 @@
+/*
+ * 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.openidentityplatform.openam.click.control;
+
+import org.openidentityplatform.openam.click.Context;
+import org.apache.click.Stateful;
+import org.apache.click.control.ActionLink;
+import org.apache.click.control.Submit;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+import org.apache.commons.lang.StringUtils;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides a Abstract Link control: <a href=""></a>.
+ *
+ * See also the W3C HTML reference:
+ * A Links
+ *
+ * @see ActionLink
+ * @see Submit
+ */
+public abstract class AbstractLink extends AbstractControl implements Stateful {
+
+ private static final long serialVersionUID = 1L;
+
+ // Instance Variables -----------------------------------------------------
+
+ /** The Field disabled value. */
+ protected boolean disabled;
+
+ /**
+ * The image src path attribute. If the image src is defined then a
+ * <img/> element will rendered inside the anchor link when
+ * using the AbstractLink {@link #toString()} method.
+ *
+ * If the image src value is prefixed with '/' then the request context path
+ * will be prefixed to the src value when rendered by the control.
+ */
+ protected String imageSrc;
+
+ /** The link display label. */
+ protected String label;
+
+ /** The link parameters map. */
+ protected Map parameters;
+
+ /** The link 'tabindex' attribute. */
+ protected int tabindex;
+
+ /** The link title attribute, which acts as a tooltip help message. */
+ protected String title;
+
+ /** Flag to set if both icon and text are rendered, default value is false. */
+ protected boolean renderLabelAndImage = false;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * Create an AbstractLink for the given name.
+ *
+ * @param name the page link name
+ * @throws IllegalArgumentException if the name is null
+ */
+ public AbstractLink(String name) {
+ setName(name);
+ }
+
+ /**
+ * Create an AbstractLink with no name defined.
+ *
+ * Please note the control's name must be defined before it is valid.
+ */
+ public AbstractLink() {
+ }
+
+ // Public Attributes ------------------------------------------------------
+
+ /**
+ * Return the link html tag: a.
+ *
+ * @see AbstractControl#getTag()
+ *
+ * @return this controls html tag
+ */
+ @Override
+ public String getTag() {
+ return "a";
+ }
+
+ /**
+ * Return true if the AbstractLink is a disabled. If the link is disabled
+ * it will be rendered as <span> element with a HTML class attribute
+ * of "disabled".
+ *
+ * @return true if the AbstractLink is a disabled
+ */
+ public boolean isDisabled() {
+ return disabled;
+ }
+
+ /**
+ * Set the disabled flag. If the link is disabled it will be rendered as
+ * <span> element with a HTML class attribute of "disabled".
+ *
+ * @param disabled the disabled flag
+ */
+ public void setDisabled(boolean disabled) {
+ this.disabled = disabled;
+ }
+
+ /**
+ * Return the AbstractLink anchor <a> tag href attribute.
+ * This method will encode the URL with the session ID
+ * if required using HttpServletResponse.encodeURL().
+ *
+ * @return the AbstractLink HTML href attribute
+ */
+ public abstract String getHref();
+
+ /**
+ * Return the image src path attribute. If the image src is defined then a
+ * <img/> element will be rendered inside the anchor link
+ * when using the AbstractLink {@link #toString()} method.
+ *
+ * Note: the label will not be rendered in this case (default behavior),
+ * unless the {@link #setRenderLabelAndImage(boolean)} flag is set to true.
+ *
+ * If the src value is prefixed with '/' then the request context path will
+ * be prefixed to the src value when rendered by the control.
+ *
+ * @return the image src path attribute
+ */
+ public String getImageSrc() {
+ return imageSrc;
+ }
+
+ /**
+ * Set the image src path attribute. If the src value is prefixed with
+ * '/' then the request context path will be prefixed to the src value when
+ * rendered by the control.
+ *
+ * If the image src is defined then an <img/> element will
+ * be rendered inside the anchor link when using the AbstractLink
+ * {@link #toString()} method.
+ *
+ * Note: the label will not be rendered in this case (default behavior),
+ * unless the {@link #setRenderLabelAndImage(boolean)} flag is set to true.
+ *
+ * @param src the image src path attribute
+ */
+ public void setImageSrc(String src) {
+ this.imageSrc = src;
+ }
+
+ /**
+ * Return the "id" attribute value if defined, or null otherwise.
+ *
+ * @see org.apache.click.Control#getId()
+ *
+ * @return HTML element identifier attribute "id" value
+ */
+ @Override
+ public String getId() {
+ if (hasAttributes()) {
+ return getAttribute("id");
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the label for the AbstractLink.
+ *
+ * If the label value is null, this method will attempt to find a
+ * localized label message in the parent messages using the key:
+ *
+ * getName() + ".label"
+ *
+ * If not found then the message will be looked up in the
+ * /click-control.properties file using the same key.
+ * If a value still cannot be found then the ActionLink name will be converted
+ * into a label using the method: {@link ClickUtils#toLabel(String)}
+ *
+ * For example given a OrderPage with the properties file
+ * OrderPage.properties:
+ *
+ *
+ * checkout.label=Checkout
+ * checkout.title=Proceed to Checkout
+ *
+ * The page ActionLink code:
+ *
+ * public class OrderPage extends Page {
+ * ActionLink checkoutLink = new ActionLink("checkout");
+ * ..
+ * }
+ *
+ * Will render the AbstractLink label and title properties as:
+ *
+ * <a href=".." title="Proceed to Checkout">Checkout</a>
+ *
+ * When a label value is not set, or defined in any properties files, then
+ * its value will be created from the Fields name.
+ *
+ * For example given the ActionLink code:
+ *
+ *
+ * ActionLink nameField = new ActionLink("deleteItem");
+ *
+ * Will render the ActionLink label as:
+ *
+ * <a href="..">Delete Item</a>
+ *
+ * Note the ActionLink label can include raw HTML to render other elements.
+ *
+ * For example the configured label:
+ *
+ *
+ *
+ * @return the label for the ActionLink
+ */
+ public String getLabel() {
+ if (label == null) {
+ label = getMessage(getName() + ".label");
+ }
+ if (label == null) {
+ label = ClickUtils.toLabel(getName());
+ }
+ return label;
+ }
+
+ /**
+ * Set the label for the ActionLink.
+ *
+ * @see #getLabel()
+ *
+ * @param label the label for the ActionLink
+ */
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ /**
+ * Return the link request parameter value for the given name, or null if
+ * the parameter value does not exist.
+ *
+ * @param name the name of request parameter
+ * @return the link request parameter value
+ */
+ public String getParameter(String name) {
+ if (hasParameters()) {
+ Object value = getParameters().get(name);
+
+ if (value instanceof String) {
+ return (String) value;
+ }
+
+ if (value instanceof String[]) {
+ String[] array = (String[]) value;
+ if (array.length >= 1) {
+ return array[0];
+ } else {
+ return null;
+ }
+ }
+
+ return (value == null ? null : value.toString());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set the link parameter with the given parameter name and value. You would
+ * generally use parameter if you were creating the entire AbstractLink
+ * programmatically and rendering it with the {@link #toString()} method.
+ *
+ * For example given the ActionLink:
+ *
+ *
+ *
+ * @param name the attribute name
+ * @param value the attribute value
+ * @throws IllegalArgumentException if name parameter is null
+ */
+ public void setParameter(String name, Object value) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+
+ if (value != null) {
+ getParameters().put(name, value);
+ } else {
+ getParameters().remove(name);
+ }
+ }
+
+ /**
+ * Return the link request parameter values for the given name, or null if
+ * the parameter values does not exist.
+ *
+ * @param name the name of request parameter
+ * @return the link request parameter values
+ */
+ public String[] getParameterValues(String name) {
+ if (hasParameters()) {
+ Object values = getParameters().get(name);
+ if (values instanceof String) {
+ return new String[] { values.toString() };
+ }
+ if (values instanceof String[]) {
+ return (String[]) values;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set the link parameter with the given parameter name and values. If the
+ * values are null, the parameter will be removed from the {@link #parameters}.
+ *
+ * @see #setParameter(String, Object)
+ *
+ * @param name the attribute name
+ * @param values the attribute values
+ * @throws IllegalArgumentException if name parameter is null
+ */
+ public void setParameterValues(String name, Object[] values) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+
+ if (values != null) {
+ getParameters().put(name, values);
+ } else {
+ getParameters().remove(name);
+ }
+ }
+
+ /**
+ * Return the AbstractLink parameters Map.
+ *
+ * @return the AbstractLink parameters Map
+ */
+ public Map getParameters() {
+ if (parameters == null) {
+ parameters = new HashMap(4);
+ }
+ return parameters;
+ }
+
+ /**
+ * Set the AbstractLink parameter map.
+ *
+ * @param parameters the link parameter map
+ */
+ public void setParameters(Map parameters) {
+ this.parameters = parameters;
+ }
+
+ /**
+ * Defines a link parameter that will have its value bound to a matching
+ * request parameter. {@link #setParameter(String, Object) setParameter}
+ * implicitly defines a parameter as well.
+ *
+ * Please note: parameters need only be defined for Ajax requests.
+ * For non-Ajax requests, all incoming request parameters
+ * are bound, whether they are defined or not. This behavior may change in a
+ * future release.
+ *
+ * Also note: link parameters are bound to request parameters
+ * during the {@link #onProcess()} event, so link parameters must be defined
+ * in the Page constructor or onInit() event.
+ *
+ * @param name the name of the parameter to define
+ */
+ public void defineParameter(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Null name parameter");
+ }
+
+ Map localParameters = getParameters();
+ if (!localParameters.containsKey(name)) {
+ localParameters.put(name, null);
+ }
+ }
+
+ /**
+ * Return true if the AbstractLink has parameters, false otherwise.
+ *
+ * @return true if the AbstractLink has parameters, false otherwise
+ */
+ public boolean hasParameters() {
+ return parameters != null && !parameters.isEmpty();
+ }
+
+ /**
+ * Return the link "tabindex" attribute value.
+ *
+ * @return the link "tabindex" attribute value
+ */
+ public int getTabIndex() {
+ return tabindex;
+ }
+
+ /**
+ * Set the link "tabindex" attribute value.
+ *
+ * @param tabindex the link "tabindex" attribute value
+ */
+ public void setTabIndex(int tabindex) {
+ this.tabindex = tabindex;
+ }
+
+ /**
+ * Return the 'title' attribute, or null if not defined. The title
+ * attribute acts like tooltip message over the link.
+ *
+ * If the title value is null, this method will attempt to find a
+ * localized label message in the parent messages using the key:
+ *
+ * getName() + ".title"
+ *
+ * If not found then the message will be looked up in the
+ * /click-control.properties file using the same key.
+ *
+ * For examle given a ItemsPage with the properties file
+ * ItemPage.properties:
+ *
+ *
+ * edit.label=Edit
+ * edit.title=Edit Item
+ *
+ * The page ActionLink code:
+ *
+ * public class ItemsPage extends Page {
+ * ActionLink editLink = new ActionLink("edit");
+ * ..
+ * }
+ *
+ * Will render the ActionLink label and title properties as:
+ *
+ * <a href=".." title="Edit Item">Edit</a>
+ *
+ * @return the 'title' attribute tooltip message
+ */
+ public String getTitle() {
+ if (title == null) {
+ title = getMessage(getName() + ".title");
+ }
+ return title;
+ }
+
+ /**
+ * Set the 'title' attribute tooltip message.
+ *
+ * @see #getTitle()
+ *
+ * @param value the 'title' attribute tooltip message
+ */
+ public void setTitle(String value) {
+ title = value;
+ }
+
+ /**
+ * Returns true if both {@link #setImageSrc(String) icon}
+ * and {@link #setLabel(String) label} are rendered,
+ * false otherwise.
+ *
+ * @return true if both icon and text are rendered,
+ * false otherwise
+ */
+ public boolean isRenderLabelAndImage() {
+ return renderLabelAndImage;
+ }
+
+ /**
+ * Sets whether both {@link #setLabel(String) label} and
+ * {@link #setImageSrc(String) icon} are rendered for this
+ * link.
+ *
+ * @param renderLabelAndImage sets the rendering type of the link.
+ */
+ public void setRenderLabelAndImage(boolean renderLabelAndImage) {
+ this.renderLabelAndImage = renderLabelAndImage;
+ }
+
+ @Override
+ public boolean isAjaxTarget(Context context) {
+ String id = getId();
+ if (id != null) {
+ return context.getRequestParameter(id) != null;
+ } else {
+ String localName = getName();
+ if (localName != null) {
+ return localName.equals(context.getRequestParameter(ActionLink.ACTION_LINK));
+ }
+ }
+ return false;
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * This method does nothing by default since AbstractLink does not bind to
+ * request values.
+ */
+ public void bindRequestValue() {
+ }
+
+ /**
+ * Return the link state. The following state is returned:
+ *
+ *
{@link #getParameters() link parameters}
+ *
+ *
+ * @return the link state
+ */
+ public Object getState() {
+ if (hasParameters()) {
+ return getParameters();
+ }
+ return null;
+ }
+
+ /**
+ * Set the link state.
+ *
+ * @param state the link state to set
+ */
+ public void setState(Object state) {
+ if (state == null) {
+ return;
+ }
+
+ Map linkState = (Map) state;
+ setParameters(linkState);
+ }
+
+ /**
+ * Render the HTML representation of the anchor link. This method
+ * will render the entire anchor link including the tags, the label and
+ * any attributes, see {@link #setAttribute(String, String)} for an
+ * example.
+ *
+ * If the image src is defined then a <img/> element will
+ * rendered inside the anchor link instead of the label property.
+ *
+ * This method invokes the abstract {@link #getHref()} method.
+ *
+ * @see #toString()
+ *
+ * @param buffer the specified buffer to render the control's output to
+ */
+ @Override
+ public void render(HtmlStringBuffer buffer) {
+
+ if (isDisabled()) {
+
+ buffer.elementStart("span");
+ buffer.appendAttribute("id", getId());
+ addStyleClass("disabled");
+ buffer.appendAttribute("class", getAttribute("class"));
+
+ if (hasAttribute("style")) {
+ buffer.appendAttribute("style", getAttribute("style"));
+ }
+
+ buffer.closeTag();
+
+ if (StringUtils.isBlank(getImageSrc())) {
+ buffer.append(getLabel());
+
+ } else {
+ renderImgTag(buffer);
+ if (isRenderLabelAndImage()) {
+ buffer.elementStart("span").closeTag();
+ buffer.append(getLabel());
+ buffer.elementEnd("span");
+ }
+ }
+
+ buffer.elementEnd("span");
+
+ } else {
+ buffer.elementStart(getTag());
+ removeStyleClass("disabled");
+ buffer.appendAttribute("href", getHref());
+ buffer.appendAttribute("id", getId());
+ buffer.appendAttributeEscaped("title", getTitle());
+ if (getTabIndex() > 0) {
+ buffer.appendAttribute("tabindex", getTabIndex());
+ }
+
+ appendAttributes(buffer);
+
+ buffer.closeTag();
+
+ if (StringUtils.isBlank(getImageSrc())) {
+ buffer.append(getLabel());
+
+ } else {
+ renderImgTag(buffer);
+ if (isRenderLabelAndImage()) {
+ buffer.elementStart("span").closeTag();
+ buffer.append(getLabel());
+ buffer.elementEnd("span");
+ }
+ }
+
+ buffer.elementEnd(getTag());
+ }
+ }
+
+ /**
+ * Remove the link state from the session for the given request context.
+ *
+ * @see #saveState(Context)
+ * @see #restoreState(Context)
+ *
+ * @param context the request context
+ */
+ public void removeState(Context context) {
+ ClickUtils.removeState(this, getName(), context);
+ }
+
+ /**
+ * Restore the link state from the session for the given request context.
+ *
+ * This method delegates to {@link #setState(Object)} to set the
+ * link restored state.
+ *
+ * @see #saveState(Context)
+ * @see #removeState(Context)
+ *
+ * @param context the request context
+ */
+ public void restoreState(Context context) {
+ ClickUtils.restoreState(this, getName(), context);
+ }
+
+ /**
+ * Save the link state to the session for the given request context.
+ *
+ * * This method delegates to {@link #getState()} to retrieve the link state
+ * to save.
+ *
+ * @see #restoreState(Context)
+ * @see #removeState(Context)
+ *
+ * @param context the request context
+ */
+ public void saveState(Context context) {
+ ClickUtils.saveState(this, getName(), context);
+ }
+
+ // Protected Methods ------------------------------------------------------
+
+ /**
+ * Render the Image tag to the buffer.
+ *
+ * @param buffer the buffer to render the image tag to
+ */
+ protected void renderImgTag(HtmlStringBuffer buffer) {
+ buffer.elementStart("img");
+ buffer.appendAttribute("border", 0);
+ buffer.appendAttribute("hspace", 2);
+ buffer.appendAttribute("class", "link");
+
+ if (getTitle() != null) {
+ buffer.appendAttributeEscaped("alt", getTitle());
+ } else {
+ buffer.appendAttributeEscaped("alt", getLabel());
+ }
+
+ String src = getImageSrc();
+ if (StringUtils.isNotBlank(src)) {
+ if (src.charAt(0) == '/') {
+ src = getContext().getRequest().getContextPath() + src;
+ }
+ buffer.appendAttribute("src", src);
+ }
+
+ buffer.elementEnd();
+ }
+
+ /**
+ * Render the given link parameters to the buffer.
+ *
+ * The parameters will be rendered as URL key/value pairs e.g:
+ * "firstname=john&lastname=smith".
+ *
+ * Multivalued parameters will be rendered with each value sharing the same
+ * key e.g: "name=john&name=susan&name=mary".
+ *
+ * The parameter value will be encoded through
+ * {@link ClickUtils#encodeUrl(Object, Context)}.
+ *
+ * @param buffer the buffer to render the parameters to
+ * @param parameters the parameters to render
+ * @param context the request context
+ */
+ protected void renderParameters(HtmlStringBuffer buffer, Map parameters,
+ Context context) {
+
+ Iterator i = parameters.keySet().iterator();
+ while (i.hasNext()) {
+ String paramName = i.next();
+ Object paramValue = getParameters().get(paramName);
+
+ // Check for multivalued parameter
+ if (paramValue instanceof String[]) {
+ String[] paramValues = (String[]) paramValue;
+ for (int j = 0; j < paramValues.length; j++) {
+ buffer.append(paramName);
+ buffer.append("=");
+ buffer.append(ClickUtils.encodeUrl(paramValues[j], context));
+ if (j < paramValues.length - 1) {
+ buffer.append("&");
+ }
+ }
+ } else {
+ if (paramValue != null) {
+ buffer.append(paramName);
+ buffer.append("=");
+ buffer.append(ClickUtils.encodeUrl(paramValue, context));
+ }
+ }
+ if (i.hasNext()) {
+ buffer.append("&");
+ }
+ }
+ }
+
+ /**
+ * This method binds the submitted request parameters to the link
+ * parameters.
+ *
+ * For non-Ajax requests this method will bind all incoming request
+ * parameters to the link. For Ajax requests this method will only bind
+ * the parameters already defined on the link.
+ *
+ * @param context the request context
+ */
+ @SuppressWarnings("unchecked")
+ protected void bindRequestParameters(Context context) {
+ HttpServletRequest request = context.getRequest();
+
+ Set parameterNames = null;
+
+ if (context.isAjaxRequest()) {
+ parameterNames = getParameters().keySet();
+ } else {
+ parameterNames = request.getParameterMap().keySet();
+ }
+
+ for (String param : parameterNames) {
+ String[] values = request.getParameterValues(param);
+ // Do not process request parameters that return null values. Null
+ // values are only returned if the request parameter is not present.
+ // A null value can only occur for Ajax requests which processes
+ // parameters defined on the link, not the incoming parameters.
+ // The reason for not processing the null value is because it would
+ // nullify parametesr that was set during onInit
+ if (values == null) {
+ continue;
+ }
+
+ if (values.length == 1) {
+ getParameters().put(param, values[0]);
+ } else {
+ getParameters().put(param, values);
+ }
+ }
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/control/ActionLink.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/ActionLink.java
new file mode 100644
index 0000000000..c90f5f0423
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/ActionLink.java
@@ -0,0 +1,517 @@
+/*
+ * 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.openidentityplatform.openam.click.control;
+
+import org.openidentityplatform.openam.click.Context;
+import org.apache.click.control.Submit;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Provides a Action Link control: <a href=""></a>.
+ *
+ *
+ *
+ * This control can render the "href" URL attribute using
+ * {@link #getHref()}, or the entire ActionLink anchor tag using
+ * {@link #toString()}.
+ *
+ * ActionLink support invoking control listeners.
+ *
+ *
ActionLink Example
+ *
+ * An example of using ActionLink to call a logout method is illustrated below:
+ *
+ *
+ * public class MyPage extends Page {
+ *
+ * public MyPage() {
+ * ActionLink link = new ActionLink("logoutLink");
+ * link.setListener(this, "onLogoutClick");
+ * addControl(link);
+ * }
+ *
+ * public boolean onLogoutClick() {
+ * if (getContext().hasSession()) {
+ * getContext().getSession().invalidate();
+ * }
+ * setRedirect(LogoutPage.class);
+ *
+ * return false;
+ * }
+ * }
+ *
+ * The corresponding template code is below. Note href is evaluated by Velocity
+ * to {@link #getHref()}:
+ *
+ *
+ * <a href="$logoutLink.href" title="Click to Logout">Logout</a>
+ *
+ * ActionLink can also support a value parameter which is accessible
+ * using {@link #getValue()}.
+ *
+ * For example a products table could include rows
+ * of products, each with a get product details ActionLink and add product
+ * ActionLink. The ActionLinks include the product's id as a parameter to
+ * the {@link #getHref(Object)} method, which is then available when the
+ * control is processed:
+ *
+ *
+ *
+ * The corresponding Page class for this template is:
+ *
+ *
+ * public class ProductsPage extends Page {
+ *
+ * public ActionLink addLink = new ActionLink("addLink", this, "onAddClick");
+ * public ActionLink detailsLink = new ActionLink("detailsLink", this, "onDetailsClick");
+ * public List productList;
+ *
+ * public boolean onAddClick() {
+ * // Get the product clicked on by the user
+ * Integer productId = addLink.getValueInteger();
+ * Product product = getProductService().getProduct(productId);
+ *
+ * // Add product to basket
+ * List basket = (List) getContext().getSessionAttribute("basket");
+ * basket.add(product);
+ * getContext().setSessionAttribute("basket", basket);
+ *
+ * return true;
+ * }
+ *
+ * public boolean onDetailsClick() {
+ * // Get the product clicked on by the user
+ * Integer productId = detailsLink.getValueInteger();
+ * Product product = getProductService().getProduct(productId);
+ *
+ * // Store the product in the request and display in the details page
+ * getContext().setRequestAttribute("product", product);
+ * setForward(ProductDetailsPage.class);
+ *
+ * return false;
+ * }
+ *
+ * public void onRender() {
+ * // Display the list of available products
+ * productList = getProductService().getProducts();
+ * }
+ * }
+ *
+ * See also the W3C HTML reference:
+ * A Links
+ *
+ * @see org.apache.click.control.AbstractLink
+ * @see Submit
+ */
+public class ActionLink extends AbstractLink {
+
+ // Constants --------------------------------------------------------------
+
+ private static final long serialVersionUID = 1L;
+
+ /** The action link parameter name: actionLink. */
+ public static final String ACTION_LINK = "actionLink";
+
+ /** The value parameter name: value. */
+ public static final String VALUE = "value";
+
+ // Instance Variables -----------------------------------------------------
+
+ /** The link is clicked. */
+ protected boolean clicked;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * Create an ActionLink for the given name.
+ *
+ * Please note the name 'actionLink' is reserved as a control request
+ * parameter name and cannot be used as the name of the control.
+ *
+ * @param name the action link name
+ * @throws IllegalArgumentException if the name is null
+ */
+ public ActionLink(String name) {
+ setName(name);
+ }
+
+ /**
+ * Create an ActionLink for the given name and label.
+ *
+ * Please note the name 'actionLink' is reserved as a control request
+ * parameter name and cannot be used as the name of the control.
+ *
+ * @param name the action link name
+ * @param label the action link label
+ * @throws IllegalArgumentException if the name is null
+ */
+ public ActionLink(String name, String label) {
+ setName(name);
+ setLabel(label);
+ }
+
+ /**
+ * Create an ActionLink for the given listener object and listener
+ * method.
+ *
+ * @param listener the listener target object
+ * @param method the listener method to call
+ * @throws IllegalArgumentException if the name, listener or method is null
+ * or if the method is blank
+ */
+ public ActionLink(Object listener, String method) {
+ if (listener == null) {
+ throw new IllegalArgumentException("Null listener parameter");
+ }
+ if (StringUtils.isBlank(method)) {
+ throw new IllegalArgumentException("Blank listener method");
+ }
+ setListener(listener, method);
+ }
+
+ /**
+ * Create an ActionLink for the given name, listener object and listener
+ * method.
+ *
+ * Please note the name 'actionLink' is reserved as a control request
+ * parameter name and cannot be used as the name of the control.
+ *
+ * @param name the action link name
+ * @param listener the listener target object
+ * @param method the listener method to call
+ * @throws IllegalArgumentException if the name, listener or method is null
+ * or if the method is blank
+ */
+ public ActionLink(String name, Object listener, String method) {
+ setName(name);
+ if (listener == null) {
+ throw new IllegalArgumentException("Null listener parameter");
+ }
+ if (StringUtils.isBlank(method)) {
+ throw new IllegalArgumentException("Blank listener method");
+ }
+ setListener(listener, method);
+ }
+
+ /**
+ * Create an ActionLink for the given name, label, listener object and
+ * listener method.
+ *
+ * Please note the name 'actionLink' is reserved as a control request
+ * parameter name and cannot be used as the name of the control.
+ *
+ * @param name the action link name
+ * @param label the action link label
+ * @param listener the listener target object
+ * @param method the listener method to call
+ * @throws IllegalArgumentException if the name, listener or method is null
+ * or if the method is blank
+ */
+ public ActionLink(String name, String label, Object listener,
+ String method) {
+
+ setName(name);
+ setLabel(label);
+ if (listener == null) {
+ throw new IllegalArgumentException("Null listener parameter");
+ }
+ if (StringUtils.isBlank(method)) {
+ throw new IllegalArgumentException("Blank listener method");
+ }
+ setListener(listener, method);
+ }
+
+ /**
+ * Create an ActionLink with no name defined. Please note the
+ * control's name must be defined before it is valid.
+ */
+ public ActionLink() {
+ }
+
+ // Public Attributes ------------------------------------------------------
+
+ /**
+ * Returns true if the ActionLink was clicked, otherwise returns false.
+ *
+ * @return true if the ActionLink was clicked, otherwise returns false.
+ */
+ public boolean isClicked() {
+ return clicked;
+ }
+
+ /**
+ * Return the ActionLink anchor <a> tag href attribute for the
+ * given value. This method will encode the URL with the session ID
+ * if required using HttpServletResponse.encodeURL().
+ *
+ * @param value the ActionLink value parameter
+ * @return the ActionLink HTML href attribute
+ */
+ public String getHref(Object value) {
+ Context context = getContext();
+ String uri = ClickUtils.getRequestURI(context.getRequest());
+
+ HtmlStringBuffer buffer =
+ new HtmlStringBuffer(uri.length() + getName().length() + 40);
+
+ buffer.append(uri);
+ buffer.append("?");
+ buffer.append(ACTION_LINK);
+ buffer.append("=");
+ buffer.append(getName());
+ if (value != null) {
+ buffer.append("&");
+ buffer.append(VALUE);
+ buffer.append("=");
+ buffer.append(ClickUtils.encodeUrl(value, context));
+ }
+
+ if (hasParameters()) {
+ for (String paramName : getParameters().keySet()) {
+ if (!paramName.equals(ACTION_LINK) && !paramName.equals(VALUE)) {
+ Object paramValue = getParameters().get(paramName);
+
+ // Check for multivalued parameter
+ if (paramValue instanceof String[]) {
+ String[] paramValues = (String[]) paramValue;
+ for (int j = 0; j < paramValues.length; j++) {
+ buffer.append("&");
+ buffer.append(paramName);
+ buffer.append("=");
+ buffer.append(ClickUtils.encodeUrl(paramValues[j],
+ context));
+ }
+ } else {
+ if (paramValue != null) {
+ buffer.append("&");
+ buffer.append(paramName);
+ buffer.append("=");
+ buffer.append(ClickUtils.encodeUrl(paramValue,
+ context));
+ }
+ }
+ }
+ }
+ }
+
+ return context.getResponse().encodeURL(buffer.toString());
+ }
+
+ /**
+ * Return the ActionLink anchor <a> tag href attribute value.
+ *
+ * @return the ActionLink anchor <a> tag HTML href attribute value
+ */
+ @Override
+ public String getHref() {
+ return getHref(getValue());
+ }
+
+ /**
+ * Set the name of the Control. Each control name must be unique in the
+ * containing Page model or the containing Form.
+ *
+ * Please note the name 'actionLink' is reserved as a control request
+ * parameter name and cannot be used as the name of the control.
+ *
+ * @see org.apache.click.Control#setName(String)
+ *
+ * @param name of the control
+ * @throws IllegalArgumentException if the name is null
+ */
+ @Override
+ public void setName(String name) {
+ if (ACTION_LINK.equals(name)) {
+ String msg = "Invalid name '" + ACTION_LINK + "'. This name is "
+ + "reserved for use as a control request parameter name";
+ throw new IllegalArgumentException(msg);
+ }
+ super.setName(name);
+ }
+
+ /**
+ * Set the parent of the ActionLink.
+ *
+ * @see org.apache.click.Control#setParent(Object)
+ *
+ * @param parent the parent of the Control
+ * @throws IllegalStateException if {@link #name} is not defined
+ * @throws IllegalArgumentException if the given parent instance is
+ * referencing this object: if (parent == this)
+ */
+ @Override
+ public void setParent(Object parent) {
+ if (parent == this) {
+ throw new IllegalArgumentException("Cannot set parent to itself");
+ }
+ if (getName() == null) {
+ String msg = "ActionLink name not defined.";
+ throw new IllegalArgumentException(msg);
+ }
+ this.parent = parent;
+ }
+
+ /**
+ * Returns the ActionLink value if the action link was processed and has
+ * a value, or null otherwise.
+ *
+ * @return the ActionLink value if the ActionLink was processed
+ */
+ public String getValue() {
+ return getParameter(VALUE);
+ }
+
+ /**
+ * Returns the action link Double value if the action link was
+ * processed and has a value, or null otherwise.
+ *
+ * @return the action link Double value if the action link was processed
+ *
+ * @throws NumberFormatException if the value cannot be parsed into a Double
+ */
+ public Double getValueDouble() {
+ String value = getValue();
+ if (value != null) {
+ return Double.valueOf(value);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the ActionLink Integer value if the ActionLink was
+ * processed and has a value, or null otherwise.
+ *
+ * @return the ActionLink Integer value if the action link was processed
+ *
+ * @throws NumberFormatException if the value cannot be parsed into an Integer
+ */
+ public Integer getValueInteger() {
+ String value = getValue();
+ if (value != null) {
+ return Integer.valueOf(value);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the ActionLink Long value if the ActionLink was
+ * processed and has a value, or null otherwise.
+ *
+ * @return the ActionLink Long value if the action link was processed
+ *
+ * @throws NumberFormatException if the value cannot be parsed into a Long
+ */
+ public Long getValueLong() {
+ String value = getValue();
+ if (value != null) {
+ return Long.valueOf(value);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set the ActionLink value.
+ *
+ * @param value the ActionLink value
+ */
+ public void setValue(String value) {
+ setParameter(VALUE, value);
+ }
+
+ /**
+ * Set the value of the ActionLink using the given object.
+ *
+ * @param object the object value to set
+ */
+ public void setValueObject(Object object) {
+ if (object != null) {
+ setValue(object.toString());
+ } else {
+ setValue(null);
+ }
+ }
+
+ /**
+ * This method binds the submitted request value to the ActionLink's
+ * value.
+ */
+ @Override
+ public void bindRequestValue() {
+ Context context = getContext();
+ if (context.isMultipartRequest()) {
+ return;
+ }
+
+ clicked = getName().equals(context.getRequestParameter(ACTION_LINK));
+
+ if (clicked) {
+ String value = context.getRequestParameter(VALUE);
+ if (value != null) {
+ setValue(value);
+ }
+ bindRequestParameters(context);
+ }
+ }
+
+ // Public Methods ---------------------------------------------------------
+
+ /**
+ * This method will set the {@link #isClicked()} property to true if the
+ * ActionLink was clicked, and if an action callback listener was set
+ * this will be invoked.
+ *
+ * @see org.apache.click.Control#onProcess()
+ *
+ * @return true to continue Page event processing or false otherwise
+ */
+ @Override
+ public boolean onProcess() {
+ bindRequestValue();
+
+ if (isClicked()) {
+ dispatchActionEvent();
+ }
+ return true;
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/control/Button.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/Button.java
new file mode 100644
index 0000000000..bdf74b6e4b
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/Button.java
@@ -0,0 +1,219 @@
+/*
+ * 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.openidentityplatform.openam.click.control;
+
+import org.openidentityplatform.openam.click.Context;
+import org.apache.click.control.Reset;
+import org.apache.click.control.Submit;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+
+/**
+ * Provides a Button control: <input type='button'/>.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * The Button control is used to render a JavaScript enabled button which can
+ * perform client side logic. The Button control provides no server side
+ * processing. If server side processing is required use {@link Submit} instead.
+ *
+ *
Button Example
+ *
+ * The example below adds a back button to a form, which when clicked returns
+ * to the previous page.
+ *
+ *
+ * Button backButton = new Button("back", " < Back ");
+ * backButton.setOnClick("history.back();");
+ * backButton.setTitle("Return to previous page");
+ * form.add(backButton);
+ *
+ * HTML output:
+ *
+ * <input type='button' name='back' value=' < Back ' onclick='history.back();'
+ * title='Return to previous page'/>
+ *
+ * See also W3C HTML reference
+ * INPUT
+ *
+ * @see Reset
+ * @see Submit
+ */
+public class Button extends Field {
+
+ // Constants --------------------------------------------------------------
+
+ private static final long serialVersionUID = 1L;
+
+ // Constructors -----------------------------------------------------------
+
+ /**
+ * Create a button with the given name.
+ *
+ * @param name the button name
+ */
+ public Button(String name) {
+ super(name);
+ }
+
+ /**
+ * Create a button with the given name and label. The button label is
+ * rendered as the HTML "value" attribute.
+ *
+ * @param name the button name
+ * @param label the button label
+ */
+ public Button(String name, String label) {
+ setName(name);
+ setLabel(label);
+ }
+
+ /**
+ * Create a button with no name defined.
+ *
+ * Please note the control's name must be defined before it is valid.
+ */
+ public Button() {
+ }
+
+ // Public Attributes ------------------------------------------------------
+
+ /**
+ * Return the button's html tag: input.
+ *
+ * @see org.apache.click.control.AbstractControl#getTag()
+ *
+ * @return this controls html tag
+ */
+ @Override
+ public String getTag() {
+ return "input";
+ }
+
+ /**
+ * Returns the button onclick attribute value, or null if not defined.
+ *
+ * @return the button onclick attribute value, or null if not defined.
+ */
+ public String getOnClick() {
+ if (attributes != null) {
+ return attributes.get("onclick");
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Sets the button onclick attribute value.
+ *
+ * @param value the onclick attribute value.
+ */
+ public void setOnClick(String value) {
+ setAttribute("onclick", value);
+ }
+
+ /**
+ * Return the input type: 'button'.
+ *
+ * @return the input type: 'button'
+ */
+ public String getType() {
+ return "button";
+ }
+
+ // Public Methods --------------------------------------------------------
+
+ /**
+ * For non Ajax requests this method returns true, as buttons by default
+ * perform no server side logic. If the button is an Ajax target and a
+ * behavior is defined, the behavior action will be invoked.
+ *
+ * @see Field#onProcess()
+ *
+ * @return true to continue Page event processing or false otherwise
+ */
+ @Override
+ public boolean onProcess() {
+ Context context = getContext();
+
+ if (context.isAjaxRequest()) {
+
+ if (isDisabled()) {
+ // Switch off disabled property if control has incoming request
+ // parameter. Normally this means the field was enabled via JS
+ if (context.hasRequestParameter(getName())) {
+ setDisabled(false);
+ } else {
+ // If field is disabled skip process event
+ return true;
+ }
+ }
+
+ if (context.hasRequestParameter(getName())) {
+ dispatchActionEvent();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @see AbstractControl#getControlSizeEst()
+ *
+ * @return the estimated rendered control size in characters
+ */
+ @Override
+ public int getControlSizeEst() {
+ return 40;
+ }
+
+ /**
+ * Render the HTML representation of the Button. Note the button label is
+ * rendered as the HTML "value" attribute.
+ *
+ * @see #toString()
+ *
+ * @param buffer the specified buffer to render the control's output to
+ */
+ @Override
+ public void render(HtmlStringBuffer buffer) {
+ buffer.elementStart(getTag());
+
+ buffer.appendAttribute("type", getType());
+ buffer.appendAttribute("name", getName());
+ buffer.appendAttribute("id", getId());
+ buffer.appendAttribute("value", getLabel());
+ buffer.appendAttribute("title", getTitle());
+ if (getTabIndex() > 0) {
+ buffer.appendAttribute("tabindex", getTabIndex());
+ }
+
+ appendAttributes(buffer);
+
+ if (isDisabled()) {
+ buffer.appendAttributeDisabled();
+ }
+
+ buffer.elementEnd();
+ }
+}
diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/control/Column.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/Column.java
new file mode 100644
index 0000000000..a9adbcf431
--- /dev/null
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/control/Column.java
@@ -0,0 +1,1596 @@
+/*
+ * 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.openidentityplatform.openam.click.control;
+
+import org.openidentityplatform.openam.click.Context;
+import org.openidentityplatform.openam.click.util.ClickUtils;
+import org.openidentityplatform.openam.click.util.HtmlStringBuffer;
+import org.apache.click.util.PropertyUtils;
+import org.apache.commons.lang.math.NumberUtils;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * Provides the Column table data <td> and table header <th>
+ * renderer.
+ *
+ *
+ *
+ *
+ *
+ * The Column object provide column definitions for the {@link org.apache.click.control.Table} object.
+ *
+ *
Column Options
+ *
+ * The Column class supports a number of rendering options which include:
+ *
+ *
+ *
{@link #autolink} - the option to automatically render href links
+ * for email and URL column values
+ *
{@link #attributes} - the CSS style attributes for the table data cell
+ *
{@link #dataClass} - the CSS class for the table data cell
+ *
{@link #dataStyles} - the CSS styles for the table data cell
+ *
{@link #decorator} - the custom column value renderer
+ *
{@link #format} - the MessageFormat pattern rendering
+ * the column value
+ *
{@link #headerClass} - the CSS class for the table header cell
+ *
{@link #headerStyles} - the CSS styles for the table header cell
+ *
{@link #headerTitle} - the table header cell value to render
+ *
{@link #sortable} - the table column sortable property
+ *
{@link #width} - the table cell width property
+ *
+ *
+ *
Format Pattern
+ *
+ * The {@link #format} property which specifies {@link MessageFormat} pattern
+ * a is very useful for formatting column values. For example to render
+ * formatted number and date values you simply specify:
+ *
+ *
+ *
+ * The support custom column value rendering you can specify a {@link Decorator}
+ * class on columns. The decorator render method is passed the table
+ * row object and the page request Context. Using the table row you can access
+ * all the column values enabling you to render a compound value composed of
+ * multiple column values. For example:
+ *
+ *
+
+ * The Context parameter of the decorator render() method enables you to
+ * render controls to provide additional functionality. For example:
+ *
+ *
+ *
+ * Column header titles can be localized using the controls parent messages.
+ * If the column header title value is null, the column will attempt to find a
+ * localized message in the parent messages using the key:
+ *
+ * getName() + ".headerTitle"
+ *
+ * If not found then the message will be looked up in the
+ * /click-control.properties file using the same key.
+ * If a value still cannot be found then the Column name will be converted
+ * into a header title using the method: {@link ClickUtils#toLabel(String)}.
+ *
+ *
+ * @see Decorator
+ * @see org.apache.click.control.Table
+ */
+public class Column implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ // ----------------------------------------------------- Instance Variables
+
+ /** The Column attributes Map. */
+ protected Map attributes;
+
+ /**
+ * The automatically hyperlink column URL and email address values flag,
+ * default value is false.
+ */
+ protected boolean autolink;
+
+ /** The column table data <td> CSS class attribute. */
+ protected String dataClass;
+
+ /** The Map of column table data <td> CSS style attributes. */
+ protected Map dataStyles;
+
+ /** The column row decorator. */
+ protected Decorator decorator;
+
+ /** The escape HTML characters flag. The default value is true. */
+ protected boolean escapeHtml = true;
+
+ /** The column message format pattern. */
+ protected String format;
+
+ /** The CSS class attribute of the column header. */
+ protected String headerClass;
+
+ /** The Map of column table header <th> CSS style attributes. */
+ protected Map headerStyles;
+
+ /** The title of the column header. */
+ protected String headerTitle;
+
+ /**
+ * The maximum column length. If maxLength is greater than 0 and the column
+ * data string length is greater than maxLength, the rendered value will be
+ * truncated with an eclipse(...).
+ *
+ * Autolinked email or URL values will not be constrained.
+ *
+ * The default value is 0.
+ */
+ protected int maxLength;
+
+ /**
+ * The optional MessageFormat used to render the column table cell value.
+ */
+ protected MessageFormat messageFormat;
+
+ /** The property name of the row object to render. */
+ protected String name;
+
+ /** The column render id attribute status. The default value is false. */
+ protected Boolean renderId;
+
+ /** The method cached for rendering column values. */
+ protected transient Map