Modified: wicket/sandbox/knopp/experimental/wicket/src/main/java/org/apache/wicket/ajaxng/request/AjaxRequestTarget.java URL: http://svn.apache.org/viewvc/wicket/sandbox/knopp/experimental/wicket/src/main/java/org/apache/wicket/ajaxng/request/AjaxRequestTarget.java?rev=688502&r1=688501&r2=688502&view=diff ============================================================================== --- wicket/sandbox/knopp/experimental/wicket/src/main/java/org/apache/wicket/ajaxng/request/AjaxRequestTarget.java (original) +++ wicket/sandbox/knopp/experimental/wicket/src/main/java/org/apache/wicket/ajaxng/request/AjaxRequestTarget.java Sun Aug 24 05:49:42 2008 @@ -16,22 +16,137 @@ */ package org.apache.wicket.ajaxng.request; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.IRequestTarget; +import org.apache.wicket.MarkupContainer; +import org.apache.wicket.Page; import org.apache.wicket.RequestCycle; +import org.apache.wicket.ResourceReference; +import org.apache.wicket.Response; +import org.apache.wicket.WicketRuntimeException; +import org.apache.wicket.ajaxng.AjaxBehavior; +import org.apache.wicket.ajaxng.json.JSONArray; +import org.apache.wicket.ajaxng.json.JSONObject; +import org.apache.wicket.behavior.IBehavior; +import org.apache.wicket.markup.html.IHeaderResponse; +import org.apache.wicket.markup.html.internal.HeaderResponse; +import org.apache.wicket.markup.html.internal.HtmlHeaderContainer; +import org.apache.wicket.markup.parser.filter.HtmlHeaderSectionHandler; +import org.apache.wicket.markup.repeater.AbstractRepeater; +import org.apache.wicket.protocol.http.WebResponse; +import org.apache.wicket.response.StringResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class AjaxRequestTarget implements IRequestTarget { private AjaxRequestTarget() { + this.component = null; + this.page = null; + this.behaviorIndex = -1; + } + + /** + * An [EMAIL PROTECTED] AjaxRequestTarget} listener that can be used to respond to various target-related + * events + * + */ + public static interface IListener + { + /** + * Triggered before ajax request target begins its response cycle + * + * @param components + * read-only list of component entries already added to the target + * @param target + * the target itself. Could be used to add components or to append/prepend + * javascript + * + */ + public void onBeforeRespond(List<ComponentEntry> components, AjaxRequestTarget target); + + /** + * Triggered after ajax request target is done with its response cycle. At this point only + * additional javascript can be output to the response using the provided + * [EMAIL PROTECTED] IJavascriptResponse} object + * + * NOTE: During this stage of processing any calls to target that manipulate the response + * (adding components, javascript) will have no effect + * + * @param components + * read-only list of component entries added to the target + * @param response + * response object that can be used to output javascript + */ + public void onAfterRespond(List<ComponentEntry> components, IJavascriptResponse response); } - - private Component component; - private int behaviorIndex; - + + /** + * An ajax javascript response that allows users to add javascript to be executed on the client + * side + * + * @author ivaynberg + */ + public static interface IJavascriptResponse + { + /** + * Adds more javascript to the ajax response that will be executed on the client side + * + * @param script + * javascript + */ + public void addJavascript(String script); + } + + private final Page page; + private final Component component; + private final int behaviorIndex; + + private final List<ComponentEntry> entries = new ArrayList<ComponentEntry>(); + + private final List<JavascriptEntry> prependJavascripts = new ArrayList<JavascriptEntry>(); + private final List<JavascriptEntry> appendJavascripts = new ArrayList<JavascriptEntry>(); + + private final List<JavascriptEntry> domReadyJavascripts = new ArrayList<JavascriptEntry>(); + + private final List<IListener> listeners = new ArrayList<IListener>(); + + private static final Logger log = LoggerFactory.getLogger(AjaxRequestTarget.class); + + private String redirect = null; + + // whether a header contribution is being rendered + private boolean headerRendering = false; + private HtmlHeaderContainer header = null; + + private IHeaderResponse headerResponse; + + /** + * Construct. + * + * @param component + * @param behaviorIndex + */ public AjaxRequestTarget(Component component, int behaviorIndex) { + if (component == null) + { + throw new IllegalArgumentException("Argument 'component' may not be null."); + } + page = component.getPage(); + if (page == null) + { + throw new IllegalArgumentException("Component must belong to a page."); + } this.component = component; this.behaviorIndex = behaviorIndex; } @@ -40,8 +155,854 @@ { } + /** + * Returns component that has behavior which initiated this Ajax request. + * + * @return component + */ + public Component getComponent() + { + return component; + } + + /** + * Entry for a single component. Allows to specify custom javascript handlers executed before + * and after replacing the component. Also the actual component replacement can be overriden. + * + * @author Matej Knopp + */ + public static class ComponentEntry + { + private final Component component; + private String beforeReplaceJavascript; + private String afterReplaceJavascript; + private String replaceJavascript; + + /** + * Construct. + * + * @param component + */ + public ComponentEntry(Component component) + { + this.component = component; + } + + /** + * Construct. + * + * @param entry + * entry to copy + */ + public ComponentEntry(ComponentEntry entry) + { + this.component = entry.component; + this.beforeReplaceJavascript = entry.beforeReplaceJavascript; + this.afterReplaceJavascript = entry.afterReplaceJavascript; + this.replaceJavascript = entry.replaceJavascript; + } + + /** + * Returns component that will be updated. + * + * @return component + */ + public Component getComponent() + { + return component; + } + + /** + * Sets the javascript executed right before replacing the component. + * <p> + * The javascript can use following variables: + * <dl> + * <dt>requestQueueItem</dt> + * <dd>RequestQueueItem instance for current request</dd> + * <dt>componentId</dt> + * <dd>MarkupId of component that is about to be replaced + * <dt>sourceComponentId</dt> + * <dd>MarkupId of component that has initiated current ajax request or <code>null</code> + * if the component is not available. + * <dt>done</dt> + * <dd>Method that javascript needs to execute after it has finished. Note that it is + * mandatory to call this method otherwise the processing pipeline will stop</dd> + * </dl> + * + * @param beforeReplaceJavascript + * the javascript + */ + public void setBeforeReplaceJavascript(String beforeReplaceJavascript) + { + this.beforeReplaceJavascript = beforeReplaceJavascript; + } + + /** + * Returns the javascript executed before replacing the component. + * + * @see #setBeforeReplaceJavascript(String) + * @return javascript + */ + public String getBeforeReplaceJavascript() + { + return beforeReplaceJavascript; + } + + /** + * Sets the javascript executed right after replacing the component. + * <p> + * The javascript can use following variables: + * <dl> + * <dt>requestQueueItem</dt> + * <dd>RequestQueueItem instance for current request</dd> + * <dt>componentId</dt> + * <dd>MarkupId of component that has been replaced + * <dt>done</dt> + * <dd>Method that javascript needs to execute after it has finished. Note that it is + * mandatory to call this method otherwise the processing pipeline will stop</dd> + * </dl> + * + * @param afterReplaceJavascript + * the javascript + */ + public void setAfterReplaceJavascript(String afterReplaceJavascript) + { + this.afterReplaceJavascript = afterReplaceJavascript; + } + + /** + * Returns the javascript executed after replacing the component. + * + * @see #setAfterReplaceJavascript(String) + * @return javascript + */ + public String getAfterReplaceJavascript() + { + return afterReplaceJavascript; + } + + /** + * Sets the javascript executed to replace the component. + * <p> + * The javascript can use following variables: + * <dl> + * <dt>requestQueueItem</dt> + * <dd>RequestQueueItem instance for current request</dd> + * <dt>componentId</dt> + * <dd>MarkupId of component that has been replaced + * <dt>markup</dt> + * <dd>The new markup that should replace current markup</dd> + * <dt>done</dt> + * <dd>Method that javascript needs to execute after the component has been replaced. Note + * that it is mandatory to call this method otherwise the processing pipeline will stop</dd> + * </dl> + * + * An example javascript: + * + * <pre> + * var element = W.$(componentId); + * W.replaceOuterHtml(element, markup); + * done(); + * </pre> + * + * @param replaceJavascript + * the javascript + */ + public void setReplaceJavascript(String replaceJavascript) + { + this.replaceJavascript = replaceJavascript; + } + + /** + * Returns the javascript executed to replace component. + * + * @see #setReplaceJavascript(String) + * @return javsacript executed to replace component + */ + public String getReplaceJavascript() + { + return replaceJavascript; + } + } + + private static class UnmodifiableComponentEntry extends ComponentEntry + { + + public UnmodifiableComponentEntry(ComponentEntry entry) + { + super(entry); + } + + @Override + public void setAfterReplaceJavascript(String javascript) + { + throw new UnsupportedOperationException("ComponentEntry is can not be modified."); + } + + @Override + public void setBeforeReplaceJavascript(String javascript) + { + throw new UnsupportedOperationException("ComponentEntry is can not be modified."); + } + + @Override + public void setReplaceJavascript(String javascript) + { + throw new UnsupportedOperationException("ComponentEntry is can not be modified."); + } + } + + private boolean isParent(Component parent, Component component) + { + Component p = component.getParent(); + while (p != null && p != parent) + { + p = p.getParent(); + } + return p == null; + } + + private void checkComponent(Component component) + { + if (component == null) + { + throw new IllegalArgumentException("Component may not be null."); + } + else if (component instanceof Page) + { + throw new IllegalArgumentException("Component cannot be a page"); + } + else if (!component.getOutputMarkupId()) + { + throw new IllegalStateException("Component " + component.getClass().getName() + + " must have setOuputMarkupId set in order to be updated via ajax."); + } + else if (component.getRenderBodyOnly()) + { + throw new IllegalStateException("Component " + component.getClass().getName() + + " must not have setRenderBodyOnly set in order to be updated via ajax."); + } + else if (component instanceof AbstractRepeater) + { + throw new IllegalArgumentException( + "Component " + + component.getClass().getName() + + " has been added to the target. This component is a repeater and cannot be repainted via ajax directly. Instead add its parent or another markup container higher in the hierarchy."); + } + } + + public boolean addComponent(ComponentEntry entry) + { + if (entry == null) + { + throw new IllegalArgumentException("Argument 'entry' may not be null."); + } + + final Component component = entry.getComponent(); + checkComponent(component); + + for (ComponentEntry e : entries) + { + if (e.getComponent() == component) + { + return false; + } + // check if component's parent is already in queue + else if (isParent(e.getComponent(), component)) + { + return false; + } + // check if new component is parent of existing component + else if (isParent(component, e.getComponent())) + { + entries.remove(e); + break; + } + } + entries.add(entry); + return true; + } + + public boolean addComponent(Component component) + { + if (component == null) + { + throw new IllegalArgumentException("Argument 'component' may not be null."); + } + return addComponent(new ComponentEntry(component)); + } + + private static class JavascriptEntry + { + private final String javascript; + private final boolean async; + + public JavascriptEntry(String javascript, boolean async) + { + this.javascript = javascript; + this.async = async; + } + + public String getJavascript() + { + return javascript; + } + + public boolean isAsync() + { + return async; + } + }; + + /** + * Adds javascript that will be evaluated on the client side before components are replaced + * <p> + * The javascript can use following variables: + * <dl> + * <dt>requestQueueItem</dt> + * <dd>RequestQueueItem instance for current request</dd> + * <dt>done</dt> + * <dd>Must be called for asynchronous javascript + * </dl> + * + * @param javascript + * javascript to be evaluated + * @param async + * indicates if the javascript should be evaluated asynchrously. If + * <code>async</code> is <code>true</code>, the javascript must invoke the + * <code>done</code> function that it gets passed for the processing queue to + * continue. + */ + public void prependJavascript(String javascript, boolean async) + { + if (javascript == null) + { + throw new IllegalArgumentException("Argument 'javascript' may not be null."); + } + prependJavascripts.add(new JavascriptEntry(javascript, async)); + } + + /** + * Adds javascript that will be evaluated on the client side before components are replaced. The + * javascript will be executed synchronously which means that the processing queue will be held + * until the javascript finishes. + * <p> + * The javascript can use following variables: + * <dl> + * <dt>requestQueueItem</dt> + * <dd>RequestQueueItem instance for current request</dd> + * </dl> + * + * @param javascript + * javascript to be evaluated + */ + public void prependJavascript(String javascript) + { + prependJavascript(javascript, false); + } + + /** + * Adds javascript that will be evaluated on the client side after components are replaced + * <p> + * The javascript can use following variables: + * <dl> + * <dt>requestQueueItem</dt> + * <dd>RequestQueueItem instance for current request</dd> + * <dt>done</dt> + * <dd>Must be called for asynchronous javascript + * </dl> + * + * @param javascript + * javascript to be evaluated + * @param async + * indicates if the javascript should be evaluated asynchrously. If + * <code>async</code> is <code>true</code>, the javascript must invoke the + * <code>done</code> function that it gets passed for the processing queue to + * continue. + */ + public void appendJavascript(String javascript, boolean async) + { + if (javascript == null) + { + throw new IllegalArgumentException("Argument 'javascript' may not be null."); + } + appendJavascripts.add(new JavascriptEntry(javascript, async)); + } + + /** + * Adds javascript that will be evaluated on the client side after components are replaced. The + * javascript will be executed synchronously which means that the processing queue will be held + * until the javascript finishes. + * <p> + * The javascript can use following variables: + * <dl> + * <dt>requestQueueItem</dt> + * <dd>RequestQueueItem instance for current request</dd> + * </dl> + * + * @param javascript + * javascript to be evaluated + */ + public void appendJavascript(String javascript) + { + appendJavascript(javascript, false); + } + + + /** + * Adds a listener to this target + * + * @param listener + */ + public void addListener(IListener listener) + { + if (listener == null) + { + throw new IllegalArgumentException("Argument `listener` cannot be null"); + } + listeners.add(listener); + } + + private List<ComponentEntry> entriesCopy() + { + List<ComponentEntry> list = new ArrayList<ComponentEntry>(entries.size()); + for (ComponentEntry e : entries) + { + list.add(new UnmodifiableComponentEntry(e)); + } + return Collections.unmodifiableList(list); + } + + private void fireOnBeforeRespondListeners(List<ComponentEntry> entries) + { + if (!listeners.isEmpty()) + { + for (IListener l : listeners) + { + l.onBeforeRespond(entries, this); + } + } + } + + private void fireOnAfterRespondListeners(List<ComponentEntry> entries) + { + // invoke onafterresponse event on listeners + if (!(listeners.isEmpty())) + { + // create response that will be used by listeners to append + // javascript + final IJavascriptResponse jsresponse = new IJavascriptResponse() + { + + public void addJavascript(String script) + { + appendJavascript(script, false); + } + }; + + for (IListener listener : listeners) + { + listener.onAfterRespond(entries, jsresponse); + } + } + } + + /** + * Header response for an ajax request. + * + * @author Matej Knopp + */ + private class AjaxHeaderResponse extends HeaderResponse + { + + private static final long serialVersionUID = 1L; + + private void checkHeaderRendering() + { + if (headerRendering == false) + { + throw new WicketRuntimeException( + "Only methods that can be called on IHeaderResponse outside renderHead() are renderOnLoadJavascript and renderOnDomReadyJavascript"); + } + } + + @Override + public void renderCSSReference(ResourceReference reference, String media) + { + checkHeaderRendering(); + super.renderCSSReference(reference, media); + } + + @Override + public void renderCSSReference(String url) + { + checkHeaderRendering(); + super.renderCSSReference(url); + } + + @Override + public void renderCSSReference(String url, String media) + { + checkHeaderRendering(); + super.renderCSSReference(url, media); + } + + @Override + public void renderJavascript(CharSequence javascript, String id) + { + checkHeaderRendering(); + super.renderJavascript(javascript, id); + } + + @Override + public void renderCSSReference(ResourceReference reference) + { + checkHeaderRendering(); + super.renderCSSReference(reference); + } + + @Override + public void renderJavascriptReference(ResourceReference reference) + { + checkHeaderRendering(); + super.renderJavascriptReference(reference); + } + + @Override + public void renderJavascriptReference(ResourceReference reference, String id) + { + checkHeaderRendering(); + super.renderJavascriptReference(reference, id); + } + + @Override + public void renderJavascriptReference(String url) + { + checkHeaderRendering(); + super.renderJavascriptReference(url); + } + + @Override + public void renderJavascriptReference(String url, String id) + { + checkHeaderRendering(); + super.renderJavascriptReference(url, id); + } + + @Override + public void renderString(CharSequence string) + { + checkHeaderRendering(); + super.renderString(string); + } + + /** + * Construct. + */ + public AjaxHeaderResponse() + { + + } + + /** + * + * @see org.apache.wicket.markup.html.internal.HeaderResponse#renderOnDomReadyJavascript(java.lang.String) + */ + @Override + public void renderOnDomReadyJavascript(String javascript) + { + List<String> token = Arrays.asList(new String[] { "javascript-event", "window", + "domready", javascript }); + if (wasRendered(token) == false) + { + domReadyJavascripts.add(new JavascriptEntry(javascript, false)); + markRendered(token); + } + } + + /** + * + * @see org.apache.wicket.markup.html.internal.HeaderResponse#renderOnLoadJavascript(java.lang.String) + */ + @Override + public void renderOnLoadJavascript(String javascript) + { + List<String> token = Arrays.asList(new String[] { "javascript-event", "window", "load", + javascript }); + if (wasRendered(token) == false) + { + // execute the javascript after all other scripts are executed + appendJavascript(javascript, false); + markRendered(token); + } + } + + /** + * + * @see org.apache.wicket.markup.html.internal.HeaderResponse#getRealResponse() + */ + @Override + protected Response getRealResponse() + { + return RequestCycle.get().getResponse(); + } + }; + + /** + * Returns the header response associated with current AjaxRequestTarget. + * + * Beware that only renderOnDomReadyJavascript and renderOnLoadJavascript can be called outside + * the renderHeader(IHeaderResponse response) method. Calls to other render** methods will + * result in an exception being thrown. + * + * @return header response + */ + public IHeaderResponse getHeaderResponse() + { + if (headerResponse == null) + { + headerResponse = new AjaxHeaderResponse(); + } + return headerResponse; + } + + /** + * Header container component for ajax header contributions + * + * @author Matej Knopp + */ + private static class AjaxHtmlHeaderContainer extends HtmlHeaderContainer + { + private static final long serialVersionUID = 1L; + + /** + * Construct. + * + * @param id + * @param target + */ + public AjaxHtmlHeaderContainer(String id, AjaxRequestTarget target) + { + super(id); + this.target = target; + } + + /** + * + * @see org.apache.wicket.markup.html.internal.HtmlHeaderContainer#newHeaderResponse() + */ + @Override + protected IHeaderResponse newHeaderResponse() + { + return target.getHeaderResponse(); + } + + private final transient AjaxRequestTarget target; + }; + + /** + * + * @param response + * @param component + */ + private void respondHeaderContribution(final Response response, final Component component) + { + // render the head of component and all it's children + + component.renderHead(header); + + if (component instanceof MarkupContainer) + { + ((MarkupContainer)component).visitChildren(new Component.IVisitor<Component>() + { + public Object component(Component component) + { + if (component.isVisible()) + { + component.renderHead(header); + return CONTINUE_TRAVERSAL; + } + else + { + return CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER; + } + } + }); + } + } + + private String respondHeaderContribution() + { + headerRendering = true; + + // create the htmlheadercontainer if needed + if (header == null) + { + header = new AjaxHtmlHeaderContainer(HtmlHeaderSectionHandler.HEADER_ID, this); + final Page page = component.getPage(); + page.addOrReplace(header); + } + + // save old response, set new + StringResponse stringResponse = new StringResponse(); + Response oldResponse = RequestCycle.get().setResponse(stringResponse); + + for (ComponentEntry e : entries) + { + respondHeaderContribution(stringResponse, component); + } + + // revert to old response + RequestCycle.get().setResponse(oldResponse); + + headerRendering = false; + return stringResponse.toString(); + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + private void prepareRender() + { + for (Iterator<ComponentEntry> i = entries.iterator(); i.hasNext();) + { + ComponentEntry entry = i.next(); + Component component = entry.getComponent(); + + final Page page = (Page)component.findParent(Page.class); + if (page == null) + { + // dont throw an exception but just ignore this component, somehow + // it got removed from the page. + log.debug("component: " + component + " with markupid: " + component.getMarkupId() + + " not rendered because it was already removed from page"); + i.remove(); + continue; + } + + checkComponent(component); + + component.prepareForRender(); + } + } + + private String renderComponent(Component component) + { + StringResponse stringResponse = new StringResponse(); + Response originalResponse = RequestCycle.get().setResponse(stringResponse); + + page.startComponentRender(component); + component.renderComponent(); + page.endComponentRender(component); + + RequestCycle.get().setResponse(originalResponse); + return stringResponse.toString(); + } + + private JSONObject renderComponentEntry(ComponentEntry componentEntry) + { + JSONObject object = new JSONObject(); + + Component component = componentEntry.getComponent(); + object.put("componentId", component.getId()); + object.put("beforeReplaceJavascript", componentEntry.getBeforeReplaceJavascript()); + object.put("afterReplaceJavascript", componentEntry.getAfterReplaceJavascript()); + object.put("replaceJavascript", componentEntry.getReplaceJavascript()); + object.put("markup", renderComponent(component)); + + return object; + } + + private JSONObject renderJavascriptEntry(JavascriptEntry javascriptEntry) + { + JSONObject object = new JSONObject(); + + object.put("async", javascriptEntry.isAsync()); + object.put("javascript", javascriptEntry.getJavascript()); + + return object; + } + public void respond(RequestCycle requestCycle) { + IBehavior behavior = component.getBehaviors().get(behaviorIndex); + if (behavior instanceof AjaxBehavior == false) + { + throw new WicketRuntimeException("Behavior must be instance of AjaxBehavior."); + } + ((AjaxBehavior)behavior).respond(this); + + List<ComponentEntry> entriesCopy = entriesCopy(); + fireOnBeforeRespondListeners(entriesCopy); + + JSONObject response = new JSONObject(); + + if (redirect != null) + { + response.put("redirect", redirect); + } + else + { + prepareRender(); + + response.put("header", respondHeaderContribution()); + + JSONArray components = new JSONArray(); + response.put("components", components); + for (ComponentEntry entry : entries) + { + components.put(renderComponentEntry(entry)); + } + + fireOnAfterRespondListeners(entries); + + JSONArray prependJavascripts = new JSONArray(); + response.put("prependJavascript", prependJavascripts); + + for (JavascriptEntry e : this.prependJavascripts) + { + prependJavascripts.put(renderJavascriptEntry(e)); + } + + JSONArray appendJavascripts = new JSONArray(); + response.put("appendJavascript", appendJavascripts); + + for (JavascriptEntry e : this.domReadyJavascripts) + { + appendJavascripts.put(renderJavascriptEntry(e)); + } + + for (JavascriptEntry e : this.appendJavascripts) + { + appendJavascripts.put(renderJavascriptEntry(e)); + } + } + + WebResponse webResponse = (WebResponse) requestCycle.getResponse(); + prepareResponse(webResponse); + + webResponse.write("if (false) ("); + webResponse.write(response.toString()); + webResponse.write(")"); + } + + private void prepareResponse(WebResponse response) + { + final Application app = Application.get(); + + // Determine encoding + final String encoding = app.getRequestCycleSettings().getResponseRequestEncoding(); + + // Set content type based on markup type for page + response.setCharacterEncoding(encoding); + response.setContentType("text/xml; charset=" + encoding); + + // Make sure it is not cached by a client + response.setHeader("Expires", "Mon, 26 Jul 1997 05:00:00 GMT"); + response.setHeader("Cache-Control", "no-cache, must-revalidate"); + response.setHeader("Pragma", "no-cache"); } public static final AjaxRequestTarget DUMMY = new AjaxRequestTarget();