WICKET-4349
Extract the code that creates the XML for Ajax responses out of 
AjaxRequestHandler


Project: http://git-wip-us.apache.org/repos/asf/wicket/repo
Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/fda7e7a0
Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/fda7e7a0
Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/fda7e7a0

Branch: refs/heads/master
Commit: fda7e7a0d5c5346e90192051ab167fbae586d660
Parents: 332cf6e
Author: Martin Tzvetanov Grigorov <[email protected]>
Authored: Fri Jan 20 10:05:48 2012 +0100
Committer: Martin Tzvetanov Grigorov <[email protected]>
Committed: Fri Jan 20 10:07:25 2012 +0100

----------------------------------------------------------------------
 .../apache/wicket/ajax/AbstractAjaxResponse.java   |  748 +++++++++++++
 .../org/apache/wicket/ajax/AjaxRequestHandler.java |  852 ++-------------
 .../org/apache/wicket/ajax/XmlAjaxResponse.java    |  230 ++++
 3 files changed, 1085 insertions(+), 745 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/wicket/blob/fda7e7a0/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractAjaxResponse.java
----------------------------------------------------------------------
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractAjaxResponse.java 
b/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractAjaxResponse.java
new file mode 100644
index 0000000..4f89b15
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/ajax/AbstractAjaxResponse.java
@@ -0,0 +1,748 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.ajax;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.Page;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+import org.apache.wicket.markup.head.OnLoadHeaderItem;
+import org.apache.wicket.markup.head.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.request.IRequestCycle;
+import org.apache.wicket.request.Response;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.http.WebResponse;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.lang.Generics;
+import org.apache.wicket.util.string.AppendingStringBuffer;
+import org.apache.wicket.util.string.Strings;
+import org.apache.wicket.util.visit.IVisit;
+import org.apache.wicket.util.visit.IVisitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A POJO-like that collects the data for the Ajax response written to the 
client
+ * and serializes it to specific String-based format (XML, JSON, ...).
+ */
+abstract class AbstractAjaxResponse
+{
+       private static final Logger LOG = 
LoggerFactory.getLogger(AbstractAjaxResponse.class);
+
+       /**
+        * A list of scripts (JavaScript) which should be executed on the 
client side before the
+        * components' replacement
+        */
+       protected final List<CharSequence> prependJavaScripts = 
Generics.newArrayList();
+
+       /**
+        * A list of scripts (JavaScript) which should be executed on the 
client side after the
+        * components' replacement
+        */
+       protected final List<CharSequence> appendJavaScripts = 
Generics.newArrayList();
+
+       /**
+        * A list of scripts (JavaScript) which should be executed on the 
client side after the
+        * components' replacement.
+        * Executed immediately after the replacement of the components, and 
before appendJavaScripts
+        */
+       protected final List<CharSequence> domReadyJavaScripts = 
Generics.newArrayList();
+
+       /**
+        * The component instances that will be rendered/replaced.
+        */
+       protected final Map<String, Component> markupIdToComponent = new 
LinkedHashMap<String, Component>();
+
+       /**
+        * A flag that indicates that components cannot be added to 
AjaxRequestTarget anymore.
+        * See https://issues.apache.org/jira/browse/WICKET-3564
+        */
+       protected transient boolean componentsFrozen;
+
+       /**
+        * Create a response for component body and javascript that will escape 
output to make it safe
+        * to use inside a CDATA block
+        */
+       protected final AjaxResponse encodingBodyResponse;
+
+       /**
+        * Response for header contribution that will escape output to make it 
safe to use inside a
+        * CDATA block
+        */
+       protected final AjaxResponse encodingHeaderResponse;
+
+       protected HtmlHeaderContainer header = null;
+
+       // whether a header contribution is being rendered
+       private boolean headerRendering = false;
+
+       private IHeaderResponse headerResponse;
+
+       /**
+        * The page which components are being updated.
+        */
+       private final Page page;
+
+       /**
+        * Constructor.
+        *
+        * @param page
+        *      the page which components are being updated.
+        */
+       public AbstractAjaxResponse(final Page page)
+       {
+               this.page = page;
+
+               Response response = page.getResponse();
+               encodingBodyResponse = new AjaxResponse(response);
+               encodingHeaderResponse = new AjaxResponse(response);
+       }
+
+       /**
+        * Serializes this object to the response.
+        *
+        * @param response
+        *      the response to write to
+        * @param encoding
+        *      the encoding for the response
+        */
+       void writeTo(final Response response, final String encoding)
+       {
+               writeHeader(response, encoding);
+
+               // invoke onbeforerespond event on listeners
+               fireOnBeforeRespondListeners();
+
+               // process added components
+               writeComponents(response, encoding);
+
+               fireOnAfterRespondListeners(response);
+
+               // queue up prepend javascripts. unlike other steps these are 
executed out of order so that
+               // components can contribute them from inside their 
onbeforerender methods.
+               Iterator<CharSequence> it = prependJavaScripts.iterator();
+               while (it.hasNext())
+               {
+                       CharSequence js = it.next();
+                       writePriorityEvaluation(response, js);
+               }
+
+               // execute the dom ready javascripts as first javascripts
+               // after component replacement
+               it = domReadyJavaScripts.iterator();
+               while (it.hasNext())
+               {
+                       CharSequence js = it.next();
+                       writeNormalEvaluation(response, js);
+               }
+
+               it = appendJavaScripts.iterator();
+               while (it.hasNext())
+               {
+                       CharSequence js = it.next();
+                       writeNormalEvaluation(response, js);
+               }
+
+               writeFooter(response, encoding);
+       }
+
+       protected abstract void fireOnAfterRespondListeners(Response response);
+
+       protected abstract void fireOnBeforeRespondListeners();
+
+       /**
+        * @param response
+        *      the response to write to
+        * @param encoding
+        *      the encoding for the response
+        */
+    protected abstract void writeFooter(Response response, String encoding);
+
+       /**
+        *
+        * @param response
+        *      the response to write to
+        * @param js
+        *      the JavaScript to evaluate
+        */
+       protected abstract void writePriorityEvaluation(Response response, 
CharSequence js);
+
+       /**
+        *
+        * @param response
+        *      the response to write to
+        * @param js
+        *      the JavaScript to evaluate
+        */
+       protected abstract void writeNormalEvaluation(Response response, 
CharSequence js);
+
+       /**
+        * Processes components added to the target. This involves attaching 
components, rendering
+        * markup into a client side xml envelope, and detaching them
+        *
+        * @param response
+        *      the response to write to
+        * @param encoding
+        *      the encoding for the response
+        */
+       private void writeComponents(Response response, String encoding)
+       {
+               componentsFrozen = true;
+
+               // process component markup
+               for (Map.Entry<String, Component> stringComponentEntry : 
markupIdToComponent.entrySet())
+               {
+                       final Component component = 
stringComponentEntry.getValue();
+                       // final String markupId = 
stringComponentEntry.getKey();
+
+                       if (!containsAncestorFor(component))
+                       {
+                               writeComponent(response, 
component.getAjaxRegionMarkupId(), component, encoding);
+                       }
+               }
+
+               if (header != null)
+               {
+                       // some header responses buffer all calls to render*** 
until close is called.
+                       // when they are closed, they do something (i.e. 
aggregate all JS resource urls to a
+                       // single url), and then "flush" (by writing to the 
real response) before closing.
+                       // to support this, we need to allow header 
contributions to be written in the close
+                       // tag, which we do here:
+                       headerRendering = true;
+                       // save old response, set new
+                       Response oldResponse = 
RequestCycle.get().setResponse(encodingHeaderResponse);
+                       encodingHeaderResponse.reset();
+
+                       // now, close the response (which may render things)
+                       header.getHeaderResponse().close();
+
+                       // revert to old response
+                       RequestCycle.get().setResponse(oldResponse);
+
+                       // write the XML tags and we're done
+                       writeHeaderContribution(response);
+                       headerRendering = false;
+               }
+       }
+
+       /**
+        * Writes a single component
+        *
+        * @param response
+        *      the response to write to
+        * @param markupId
+        *      the markup id to use for the component replacement
+        * @param component
+        *      the component which markup will be used as replacement
+        * @param encoding
+        *      the encoding for the response
+        */
+       protected abstract void writeComponent(Response response, String 
markupId, Component component, String encoding);
+
+       /**
+        * Writes the head part of the response.
+        * For example XML preamble
+        *
+        * @param response
+        *      the response to write to
+        * @param encoding
+        *      the encoding for the response
+        */
+       protected abstract void writeHeader(Response response, String encoding);
+
+       /**
+        * Writes header contribution (<link/> or <script/>) to the response.
+        *
+        * @param response
+        *      the response to write to
+        */
+       protected abstract void writeHeaderContribution(Response response);
+
+       @Override
+       public boolean equals(Object o)
+       {
+               if (this == o) return true;
+               if (o == null || getClass() != o.getClass()) return false;
+
+               AbstractAjaxResponse that = (AbstractAjaxResponse) o;
+
+               if (!appendJavaScripts.equals(that.appendJavaScripts)) return 
false;
+               if (!domReadyJavaScripts.equals(that.domReadyJavaScripts)) 
return false;
+               return prependJavaScripts.equals(that.prependJavaScripts);
+       }
+
+       @Override
+       public int hashCode()
+       {
+               int result = prependJavaScripts.hashCode();
+               result = 31 * result + appendJavaScripts.hashCode();
+               result = 31 * result + domReadyJavaScripts.hashCode();
+               return result;
+       }
+
+       /**
+        * Adds script to the ones which are executed after the component 
replacement.
+        *
+        * @param javascript
+        *      the javascript to execute
+        */
+       final void appendJavaScript(final CharSequence javascript)
+       {
+               Args.notNull(javascript, "javascript");
+
+               appendJavaScripts.add(javascript);
+       }
+
+       /**
+        * Adds script to the ones which are executed before the component 
replacement.
+        *
+        * @param javascript
+        *      the javascript to execute
+        */
+       final void prependJavaScript(CharSequence javascript)
+       {
+               Args.notNull(javascript, "javascript");
+
+               prependJavaScripts.add(javascript);
+       }
+
+       /**
+        * Adds a component to be updated at the client side with its current 
markup
+        *
+        * @param component
+        *      the component to update
+        * @param markupId
+        *      the markup id to use to find the component in the page's markup
+        * @throws IllegalArgumentException
+        *      thrown when a Page or an AbstractRepeater is added
+        * @throws IllegalStateException
+        *      thrown when components no more can be added for replacement.
+        */
+       final void add(final Component component, final String markupId)
+                       throws IllegalArgumentException, IllegalStateException
+       {
+               Args.notEmpty(markupId, "markupId");
+               Args.notNull(component, "component");
+
+               if (component instanceof Page)
+               {
+                       if (component != page)
+                       {
+                               throw new IllegalArgumentException("component 
cannot be a page");
+                       }
+               }
+               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.");
+               }
+
+               assertComponentsNotFrozen();
+
+               component.setMarkupId(markupId);
+               markupIdToComponent.put(markupId, component);
+       }
+
+       /**
+        * @return a read-only collection of all components which have been 
added for replacement so far.
+        */
+       final Collection<? extends Component> getComponents()
+       {
+               return 
Collections.unmodifiableCollection(markupIdToComponent.values());
+       }
+
+       /**
+        * Detaches the page if at least one of its components was updated.
+        *
+        * @param requestCycle
+        *      the current request cycle
+        */
+       void detach(IRequestCycle requestCycle)
+       {
+               Iterator<Component> iterator = 
markupIdToComponent.values().iterator();
+               while (iterator.hasNext())
+               {
+                       final Component component = iterator.next();
+                       final Page parentPage = 
component.findParent(Page.class);
+                       if (parentPage != null)
+                       {
+                               parentPage.detach();
+                               break;
+                       }
+               }
+       }
+
+       /**
+        * Checks if the target contains an ancestor for the given component
+        *
+        * @param component
+        *      the component which ancestors should be checked.
+        * @return <code>true</code> if target contains an ancestor for the 
given component
+        */
+       protected boolean containsAncestorFor(Component component)
+       {
+               Component cursor = component.getParent();
+               while (cursor != null)
+               {
+                       if (markupIdToComponent.containsValue(cursor))
+                       {
+                               return true;
+                       }
+                       cursor = cursor.getParent();
+               }
+               return false;
+       }
+
+       /**
+        * @return {@code true} if the page has been added for replacement
+        */
+       boolean containsPage()
+       {
+               return markupIdToComponent.values().contains(page);
+       }
+
+       /**
+        * Gets or creates an IHeaderResponse instance to use for the header 
contributions.
+        *
+        * @return IHeaderResponse instance to use for the header contributions.
+        */
+       IHeaderResponse getHeaderResponse()
+       {
+               if (headerResponse == null)
+               {
+                       // we don't need to decorate the header response here 
because this is called from
+                       // within AjaxHtmlHeaderContainer, which decorates the 
response
+                       headerResponse = new AjaxHeaderResponse();
+               }
+               return headerResponse;
+       }
+
+       /**
+        * @param response
+        *      the response to write to
+        * @param component
+        *      to component which will contribute to the header
+        */
+       protected void writeHeaderContribution(final Response response, final 
Component component)
+       {
+               headerRendering = true;
+
+               // create the htmlheadercontainer if needed
+               if (header == null)
+               {
+                       header = new AjaxHtmlHeaderContainer(this);
+                       final Page parentPage = component.getPage();
+                       parentPage.addOrReplace(header);
+               }
+
+               RequestCycle requestCycle = component.getRequestCycle();
+
+               // save old response, set new
+               Response oldResponse = 
requestCycle.setResponse(encodingHeaderResponse);
+
+               try {
+                       encodingHeaderResponse.reset();
+
+                       // render the head of component and all it's children
+
+                       component.renderHead(header);
+
+                       if (component instanceof MarkupContainer)
+                       {
+                               ((MarkupContainer)component).visitChildren(new 
IVisitor<Component, Void>()
+                               {
+                                       @Override
+                                       public void component(final Component 
component, final IVisit<Void> visit)
+                                       {
+                                               if 
(component.isVisibleInHierarchy())
+                                               {
+                                                       
component.renderHead(header);
+                                               }
+                                               else
+                                               {
+                                                       visit.dontGoDeeper();
+                                               }
+                                       }
+                               });
+                       }
+               } finally {
+                       // revert to old response
+                       requestCycle.setResponse(oldResponse);
+               }
+
+               writeHeaderContribution(response);
+
+               headerRendering = false;
+       }
+
+       /**
+        * Sets the Content-Type header to indicate the type of the Ajax 
response.
+        *
+        * @param response
+        *      the current we response
+        * @param encoding
+        *      the encoding to use
+        */
+       protected abstract void setContentType(WebResponse response, String 
encoding);
+
+
+       /**
+        * Header container component for ajax header contributions
+        *
+        * @author Matej Knopp
+        */
+       private static class AjaxHtmlHeaderContainer extends HtmlHeaderContainer
+       {
+               private static final long serialVersionUID = 1L;
+
+               private final transient AbstractAjaxResponse ajaxResponse;
+
+               /**
+                * Constructor.
+                *
+                * @param ajaxResponse
+                *      the object that keeps the data for the Ajax response
+                */
+               public AjaxHtmlHeaderContainer(final AbstractAjaxResponse 
ajaxResponse)
+               {
+                       super(HtmlHeaderSectionHandler.HEADER_ID);
+                       this.ajaxResponse = ajaxResponse;
+               }
+
+               /**
+                *
+                * @see 
org.apache.wicket.markup.html.internal.HtmlHeaderContainer#newHeaderResponse()
+                */
+               @Override
+               protected IHeaderResponse newHeaderResponse()
+               {
+                       return ajaxResponse.getHeaderResponse();
+               }
+       }
+
+       /**
+        * Header response for an ajax request.
+        *
+        * @author Matej Knopp
+        */
+       private class AjaxHeaderResponse extends HeaderResponse
+       {
+               @Override
+               public void render(HeaderItem item)
+               {
+                       if (item instanceof OnLoadHeaderItem)
+                       {
+                               if (!wasItemRendered(item))
+                               {
+                                       
AbstractAjaxResponse.this.appendJavaScript(((OnLoadHeaderItem) 
item).getJavaScript());
+                                       markItemRendered(item);
+                               }
+                       }
+                       else if (item instanceof OnDomReadyHeaderItem)
+                       {
+                               if (!wasItemRendered(item))
+                               {
+                                       
AbstractAjaxResponse.this.domReadyJavaScripts.add(((OnDomReadyHeaderItem)item).getJavaScript());
+                                       markItemRendered(item);
+                               }
+                       }
+                       else if (headerRendering)
+                       {
+                               super.render(item);
+                       }
+                       else
+                       {
+                               LOG.debug("Only methods that can be called on 
IHeaderResponse outside renderHead() are renderOnLoadJavaScript and 
renderOnDomReadyJavaScript");
+                       }
+               }
+
+               protected Response getRealResponse()
+               {
+                       return RequestCycle.get().getResponse();
+               }
+       }
+
+       /**
+        * Response that uses an encoder to encode its contents
+        *
+        * @author Igor Vaynberg (ivaynberg)
+        */
+       protected static final class AjaxResponse extends Response
+       {
+               private final AppendingStringBuffer buffer = new 
AppendingStringBuffer(256);
+
+               private boolean escaped = false;
+
+               private final Response originalResponse;
+
+               /**
+                * Constructor.
+                *
+                * @param originalResponse
+                *      the original request cycle response
+                */
+               private AjaxResponse(Response originalResponse)
+               {
+                       this.originalResponse = originalResponse;
+               }
+
+               /**
+                * @see 
org.apache.wicket.request.Response#encodeURL(CharSequence)
+                */
+               @Override
+               public String encodeURL(CharSequence url)
+               {
+                       return originalResponse.encodeURL(url);
+               }
+
+               /**
+                * @return contents of the response
+                */
+               public CharSequence getContents()
+               {
+                       return buffer;
+               }
+
+               /**
+                * @return true if any escaping has been performed, false 
otherwise
+                */
+               public boolean isContentsEncoded()
+               {
+                       return escaped;
+               }
+
+               /**
+                * @see org.apache.wicket.request.Response#write(CharSequence)
+                */
+               @Override
+               public void write(CharSequence cs)
+               {
+                       String string = cs.toString();
+                       if (needsEncoding(string))
+                       {
+                               string = encode(string);
+                               escaped = true;
+                               buffer.append(string);
+                       }
+                       else
+                       {
+                               buffer.append(cs);
+                       }
+               }
+
+               /**
+                * Resets the response to a clean state so it can be reused to 
save on garbage.
+                */
+               @Override
+               public void reset()
+               {
+                       buffer.clear();
+                       escaped = false;
+               }
+
+               @Override
+               public void write(byte[] array)
+               {
+                       throw new UnsupportedOperationException("Cannot write 
binary data.");
+               }
+
+               @Override
+               public void write(byte[] array, int offset, int length)
+               {
+                       throw new UnsupportedOperationException("Cannot write 
binary data.");
+               }
+
+               @Override
+               public Object getContainerResponse()
+               {
+                       return originalResponse.getContainerResponse();
+               }
+       }
+
+       /**
+        * Encodes a string so it is safe to use inside CDATA blocks
+        *
+        * @param str
+        *      the string to encode.
+        * @return encoded string
+        */
+       static String encode(CharSequence str)
+       {
+               if (str == null)
+               {
+                       return null;
+               }
+
+               return Strings.replaceAll(str, "]", "]^").toString();
+       }
+
+       /**
+        * @return name of encoding used to possibly encode the contents of the 
CDATA blocks
+        */
+       protected String getEncodingName()
+       {
+               return "wicket1";
+       }
+
+       /**
+        *
+        * @param str
+        *      the string to check
+        * @return {@code true} if string needs to be encoded, {@code false} 
otherwise
+        */
+       static boolean needsEncoding(CharSequence str)
+       {
+               /*
+                * TODO Post 1.2: Ajax: we can improve this by keeping a buffer 
of at least 3 characters and
+                * checking that buffer so that we can narrow down escaping 
occurring only for ']]>'
+                * sequence, or at least for ]] if ] is the last char in this 
buffer.
+                *
+                * but this improvement will only work if we write first and 
encode later instead of working
+                * on fragments sent to write
+                */
+               return Strings.indexOf(str, ']') >= 0;
+       }
+
+       private void assertComponentsNotFrozen()
+       {
+               assertNotFrozen(componentsFrozen, Component.class);
+       }
+
+       private void assertNotFrozen(boolean frozen, Class<?> clazz)
+       {
+               if (frozen)
+               {
+                       throw new IllegalStateException(clazz.getSimpleName() + 
"s can no " +
+                                       " longer be added");
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/fda7e7a0/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxRequestHandler.java
----------------------------------------------------------------------
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxRequestHandler.java 
b/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxRequestHandler.java
index cb95e09..e505006 100644
--- a/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxRequestHandler.java
+++ b/wicket-core/src/main/java/org/apache/wicket/ajax/AjaxRequestHandler.java
@@ -19,8 +19,6 @@ package org.apache.wicket.ajax;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -32,11 +30,6 @@ import org.apache.wicket.MarkupContainer;
 import org.apache.wicket.Page;
 import org.apache.wicket.event.Broadcast;
 import org.apache.wicket.markup.head.IHeaderResponse;
-import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
-import org.apache.wicket.markup.head.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.request.IRequestCycle;
 import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.Response;
@@ -48,12 +41,9 @@ import org.apache.wicket.request.handler.logger.PageLogData;
 import org.apache.wicket.request.http.WebRequest;
 import org.apache.wicket.request.http.WebResponse;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
-import org.apache.wicket.markup.head.HeaderItem;
-import org.apache.wicket.markup.head.OnLoadHeaderItem;
 import org.apache.wicket.response.StringResponse;
 import org.apache.wicket.response.filter.IResponseFilter;
 import org.apache.wicket.util.lang.Args;
-import org.apache.wicket.util.lang.Generics;
 import org.apache.wicket.util.string.AppendingStringBuffer;
 import org.apache.wicket.util.string.Strings;
 import org.apache.wicket.util.visit.IVisit;
@@ -92,125 +82,13 @@ import org.slf4j.LoggerFactory;
 public class AjaxRequestHandler implements AjaxRequestTarget
 {
 
-       /**
-        * Response that uses an encoder to encode its contents
-        *
-        * @author Igor Vaynberg (ivaynberg)
-        */
-       private static final class AjaxResponse extends Response
-       {
-               private final AppendingStringBuffer buffer = new 
AppendingStringBuffer(256);
-
-               private boolean escaped = false;
-
-               private final Response originalResponse;
-
-               /**
-                * Construct.
-                *
-                * @param originalResponse
-                */
-               private AjaxResponse(Response originalResponse)
-               {
-                       this.originalResponse = originalResponse;
-               }
-
-               /**
-                * @see 
org.apache.wicket.request.Response#encodeURL(CharSequence)
-                */
-               @Override
-               public String encodeURL(CharSequence url)
-               {
-                       return originalResponse.encodeURL(url);
-               }
-
-               /**
-                * @return contents of the response
-                */
-               public CharSequence getContents()
-               {
-                       return buffer;
-               }
-
-               /**
-                * @return true if any escaping has been performed, false 
otherwise
-                */
-               public boolean isContentsEncoded()
-               {
-                       return escaped;
-               }
-
-               /**
-                * @see org.apache.wicket.request.Response#write(CharSequence)
-                */
-               @Override
-               public void write(CharSequence cs)
-               {
-                       String string = cs.toString();
-                       if (needsEncoding(string))
-                       {
-                               string = encode(string);
-                               escaped = true;
-                               buffer.append(string);
-                       }
-                       else
-                       {
-                               buffer.append(cs);
-                       }
-               }
-
-               /**
-                * Resets the response to a clean state so it can be reused to 
save on garbage.
-                */
-               @Override
-               public void reset()
-               {
-                       buffer.clear();
-                       escaped = false;
-               }
-
-               @Override
-               public void write(byte[] array)
-               {
-                       throw new UnsupportedOperationException("Cannot write 
binary data.");
-               }
-
-               @Override
-               public void write(byte[] array, int offset, int length)
-               {
-                       throw new UnsupportedOperationException("Cannot write 
binary data.");
-               }
-
-               @Override
-               public Object getContainerResponse()
-               {
-                       return originalResponse.getContainerResponse();
-               }
-       }
-
-       private static final Logger log = 
LoggerFactory.getLogger(AjaxRequestHandler.class);
-
-       private final List<CharSequence> appendJavaScripts = 
Generics.newArrayList();
-
-       private final List<CharSequence> domReadyJavaScripts = 
Generics.newArrayList();
+       private static final Logger LOG = 
LoggerFactory.getLogger(AjaxRequestHandler.class);
 
        /**
-        * Create a response for component body and javascript that will escape 
output to make it safe
-        * to use inside a CDATA block
+        * A POJO-like that collects the data for the Ajax response written to 
the client
+        * and serializes it to specific String-based format (XML, JSON, ...).
         */
-       private final AjaxResponse encodingBodyResponse;
-
-       /**
-        * Response for header contribution that will escape output to make it 
safe to use inside a
-        * CDATA block
-        */
-       private final AjaxResponse encodingHeaderResponse;
-
-       /** the component instances that will be rendered */
-       private final Map<String, Component> markupIdToComponent = new 
LinkedHashMap<String, Component>();
-
-       /** */
-       private final List<CharSequence> prependJavaScripts = 
Generics.newArrayList();
+       private final AbstractAjaxResponse responseObject;
 
        /** a list of listeners */
        private List<AjaxRequestTarget.IListener> listeners = null;
@@ -218,14 +96,13 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
        /** */
        private final Set<ITargetRespondListener> respondListeners = new 
HashSet<ITargetRespondListener>();
 
+       /** see https://issues.apache.org/jira/browse/WICKET-3564 */
+       protected transient boolean respondersFrozen;
+       protected transient boolean listenersFrozen;
+
        /** The associated Page */
        private final Page page;
 
-       /** see https://issues.apache.org/jira/browse/WICKET-3564 */
-       private transient boolean componentsFrozen;
-       private transient boolean listenersFrozen;
-       private transient boolean respondersFrozen;
-
        private PageLogData logData;
 
        /**
@@ -233,12 +110,69 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
         *
         * @param page
         */
-       public AjaxRequestHandler(Page page)
+       public AjaxRequestHandler(final Page page)
        {
                this.page = Args.notNull(page, "page");
-               Response response = page.getResponse();
-               encodingBodyResponse = new AjaxResponse(response);
-               encodingHeaderResponse = new AjaxResponse(response);
+
+               responseObject =  new XmlAjaxResponse(page) {
+
+                       /**
+                        * Freezes the {@link AjaxRequestHandler#listeners}, 
and does not un-freeze them as the events will have been
+                        * fired by now.
+                        *
+                        * @param response
+                        */
+                       @Override
+                       protected void fireOnAfterRespondListeners(final 
Response response)
+                       {
+                               listenersFrozen = true;
+
+                               // invoke onafterresponse event on listeners
+                               if (listeners != null)
+                               {
+                                       final Map<String, Component> components 
= Collections.unmodifiableMap(markupIdToComponent);
+
+                                       // create response that will be used by 
listeners to append
+                                       // javascript
+                                       final 
AjaxRequestTarget.IJavaScriptResponse jsresponse = new 
AjaxRequestTarget.IJavaScriptResponse()
+                                       {
+                                               @Override
+                                               public void 
addJavaScript(String script)
+                                               {
+                                                       
writeNormalEvaluation(response, script);
+                                               }
+                                       };
+
+                                       for (AjaxRequestTarget.IListener 
listener : listeners)
+                                       {
+                                               
listener.onAfterRespond(components, jsresponse);
+                                       }
+                               }
+                       }
+
+                       /**
+                        * Freezes the {@link AjaxRequestHandler#listeners} 
before firing the event and un-freezes them afterwards to
+                        * allow components to add more {@link 
AjaxRequestTarget.IListener}s for the second event.
+                        */
+                       @Override
+                       protected void fireOnBeforeRespondListeners()
+                       {
+                               listenersFrozen = true;
+
+                               if (listeners != null)
+                               {
+                                       final Map<String, Component> components 
= Collections.unmodifiableMap(markupIdToComponent);
+
+                                       for (AjaxRequestTarget.IListener 
listener : listeners)
+                                       {
+                                               
listener.onBeforeRespond(components, AjaxRequestHandler.this);
+                                       }
+                               }
+
+                               listenersFrozen = false;
+                       }
+
+               };
        }
 
        /**
@@ -250,30 +184,6 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
                return page;
        }
 
-       private void assertNotFrozen(boolean frozen, Class<?> clazz)
-       {
-               if (frozen)
-               {
-                       throw new IllegalStateException(clazz.getSimpleName() + 
"s can no " +
-                               " longer be added");
-               }
-       }
-
-       private void assertListenersNotFrozen()
-       {
-               assertNotFrozen(listenersFrozen, 
AjaxRequestTarget.IListener.class);
-       }
-
-       private void assertComponentsNotFrozen()
-       {
-               assertNotFrozen(componentsFrozen, Component.class);
-       }
-
-       private void assertRespondersNotFrozen()
-       {
-               assertNotFrozen(respondersFrozen, ITargetRespondListener.class);
-       }
-
        @Override
        public void addListener(AjaxRequestTarget.IListener listener) throws 
IllegalStateException
        {
@@ -326,38 +236,15 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
        }
 
        @Override
-       public final void add(final Component component, final String markupId)
-               throws IllegalArgumentException, IllegalStateException
+       public void add(Component component, String markupId)
        {
-               Args.notEmpty(markupId, "markupId");
-               Args.notNull(component, "component");
-
-               if (component instanceof Page)
-               {
-                       if (component != page)
-                       {
-                               throw new IllegalArgumentException("component 
cannot be a page");
-                       }
-               }
-               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.");
-               }
-
-               assertComponentsNotFrozen();
-
-               component.setMarkupId(markupId);
-               markupIdToComponent.put(markupId, component);
+               responseObject.add(component, markupId);
        }
 
        @Override
        public final Collection<? extends Component> getComponents()
        {
-               return 
Collections.unmodifiableCollection(markupIdToComponent.values());
+               return responseObject.getComponents();
        }
 
        @Override
@@ -376,9 +263,7 @@ public class AjaxRequestHandler implements AjaxRequestTarget
        @Override
        public final void appendJavaScript(CharSequence javascript)
        {
-               Args.notNull(javascript, "javascript");
-
-               appendJavaScripts.add(javascript);
+               responseObject.appendJavaScript(javascript);
        }
 
        /**
@@ -388,18 +273,11 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
        public void detach(final IRequestCycle requestCycle)
        {
                if (logData == null)
-                       logData = new PageLogData(page);
-
-               // detach the page if it was updated
-               if (markupIdToComponent.size() > 0)
                {
-                       final Component component = 
markupIdToComponent.values().iterator().next();
-                       final Page page = component.findParent(Page.class);
-                       if (page != null)
-                       {
-                               page.detach();
-                       }
+                       logData = new PageLogData(page);
                }
+
+               responseObject.detach(requestCycle);
        }
 
        /**
@@ -411,9 +289,7 @@ public class AjaxRequestHandler implements AjaxRequestTarget
                if (obj instanceof AjaxRequestHandler)
                {
                        AjaxRequestHandler that = (AjaxRequestHandler)obj;
-                       return 
markupIdToComponent.equals(that.markupIdToComponent) &&
-                               
prependJavaScripts.equals(that.prependJavaScripts) &&
-                               
appendJavaScripts.equals(that.appendJavaScripts);
+                       return responseObject.equals(that.responseObject);
                }
                return false;
        }
@@ -425,18 +301,14 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
        public int hashCode()
        {
                int result = "AjaxRequestHandler".hashCode();
-               result += markupIdToComponent.hashCode() * 17;
-               result += prependJavaScripts.hashCode() * 17;
-               result += appendJavaScripts.hashCode() * 17;
+               result += responseObject.hashCode() * 17;
                return result;
        }
 
        @Override
        public final void prependJavaScript(CharSequence javascript)
        {
-               Args.notNull(javascript, "javascript");
-
-               prependJavaScripts.add(javascript);
+               responseObject.prependJavaScript(javascript);
        }
 
        @Override
@@ -455,7 +327,7 @@ public class AjaxRequestHandler implements AjaxRequestTarget
                final RequestCycle rc = (RequestCycle)requestCycle;
                final WebResponse response = 
(WebResponse)requestCycle.getResponse();
 
-               if (markupIdToComponent.values().contains(page))
+               if (responseObject.containsPage())
                {
                        // the page itself has been added to the request 
target, we simply issue a redirect
                        // back to the page
@@ -472,7 +344,7 @@ public class AjaxRequestHandler implements AjaxRequestTarget
                        listener.onTargetRespond(this);
                }
 
-               final Application app = Application.get();
+               final Application app = page.getApplication();
 
                page.send(app, Broadcast.BREADTH, this);
 
@@ -480,7 +352,7 @@ public class AjaxRequestHandler implements AjaxRequestTarget
                final String encoding = 
app.getRequestCycleSettings().getResponseRequestEncoding();
 
                // Set content type based on markup type for page
-               response.setContentType("text/xml; charset=" + encoding);
+               responseObject.setContentType(response, encoding);
 
                // Make sure it is not cached by a client
                response.disableCaching();
@@ -488,67 +360,15 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
                try
                {
                        final StringResponse bodyResponse = new 
StringResponse();
-                       constructResponseBody(bodyResponse, encoding);
+                       responseObject.writeTo(bodyResponse, encoding);
                        CharSequence filteredResponse = 
invokeResponseFilters(bodyResponse);
                        response.write(filteredResponse);
                }
                finally
                {
                        // restore the original response
-                       RequestCycle.get().setResponse(response);
-               }
-       }
-
-       /**
-        * Collects the response body (without the headers) so that it can be 
pre-processed before
-        * written down to the original response.
-        *
-        * @param bodyResponse
-        *            the buffering response
-        * @param encoding
-        *            the encoding that should be used to encode the body
-        */
-       private void constructResponseBody(final Response bodyResponse, final 
String encoding)
-       {
-               bodyResponse.write("<?xml version=\"1.0\" encoding=\"");
-               bodyResponse.write(encoding);
-               bodyResponse.write("\"?>");
-               bodyResponse.write("<ajax-response>");
-
-               // invoke onbeforerespond event on listeners
-               fireOnBeforeRespondListeners();
-
-               // process added components
-               respondComponents(bodyResponse);
-
-               fireOnAfterRespondListeners(bodyResponse);
-
-               // queue up prepend javascripts. unlike other steps these are 
executed out of order so that
-               // components can contribute them from inside their 
onbeforerender methods.
-               Iterator<CharSequence> it = prependJavaScripts.iterator();
-               while (it.hasNext())
-               {
-                       CharSequence js = it.next();
-                       respondPriorityInvocation(bodyResponse, js);
+                       rc.setResponse(response);
                }
-
-
-               // execute the dom ready javascripts as first javascripts
-               // after component replacement
-               it = domReadyJavaScripts.iterator();
-               while (it.hasNext())
-               {
-                       CharSequence js = it.next();
-                       respondInvocation(bodyResponse, js);
-               }
-               it = appendJavaScripts.iterator();
-               while (it.hasNext())
-               {
-                       CharSequence js = it.next();
-                       respondInvocation(bodyResponse, js);
-               }
-
-               bodyResponse.write("</ajax-response>");
        }
 
        /**
@@ -578,505 +398,28 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
        }
 
        /**
-        * Freezes the {@link #listeners} before firing the event and 
un-freezes them afterwards to
-        * allow components to add more {@link AjaxRequestTarget.IListener}s 
for the second event.
-        */
-       private void fireOnBeforeRespondListeners()
-       {
-               listenersFrozen = true;
-
-               if (listeners != null)
-               {
-                       final Map<String, Component> components = 
Collections.unmodifiableMap(markupIdToComponent);
-
-                       for (AjaxRequestTarget.IListener listener : listeners)
-                       {
-                               listener.onBeforeRespond(components, this);
-                       }
-               }
-
-               listenersFrozen = false;
-       }
-
-       /**
-        * Freezes the {@link #listeners}, and does not un-freeze them as the 
events will have been
-        * fired by now.
-        *
-        * @param response
-        */
-       private void fireOnAfterRespondListeners(final Response response)
-       {
-               listenersFrozen = true;
-
-               // invoke onafterresponse event on listeners
-               if (listeners != null)
-               {
-                       final Map<String, Component> components = 
Collections.unmodifiableMap(markupIdToComponent);
-
-                       // create response that will be used by listeners to 
append
-                       // javascript
-                       final IJavaScriptResponse jsresponse = new 
IJavaScriptResponse()
-                       {
-                               @Override
-                               public void addJavaScript(String script)
-                               {
-                                       respondInvocation(response, script);
-                               }
-                       };
-
-                       for (AjaxRequestTarget.IListener listener : listeners)
-                       {
-                               listener.onAfterRespond(components, jsresponse);
-                       }
-               }
-       }
-
-       /**
-        * Processes components added to the target. This involves attaching 
components, rendering
-        * markup into a client side xml envelope, and detaching them
-        *
-        * @param response
-        */
-       private void respondComponents(Response response)
-       {
-               componentsFrozen = true;
-               // TODO: We might need to call prepareRender on all components 
upfront
-
-               // process component markup
-               for (Map.Entry<String, Component> stringComponentEntry : 
markupIdToComponent.entrySet())
-               {
-                       final Component component = 
stringComponentEntry.getValue();
-                       // final String markupId = 
stringComponentEntry.getKey();
-
-                       if (!containsAncestorFor(component))
-                       {
-                               respondComponent(response, 
component.getAjaxRegionMarkupId(), component);
-                       }
-               }
-
-               if (header != null)
-               {
-                       // some header responses buffer all calls to render*** 
until close is called.
-                       // when they are closed, they do something (i.e. 
aggregate all JS resource urls to a
-                       // single url), and then "flush" (by writing to the 
real response) before closing.
-                       // to support this, we need to allow header 
contributions to be written in the close
-                       // tag, which we do here:
-                       headerRendering = true;
-                       // save old response, set new
-                       Response oldResponse = 
RequestCycle.get().setResponse(encodingHeaderResponse);
-                       encodingHeaderResponse.reset();
-
-                       // now, close the response (which may render things)
-                       header.getHeaderResponse().close();
-
-                       // revert to old response
-                       RequestCycle.get().setResponse(oldResponse);
-
-                       // write the XML tags and we're done
-                       writeHeaderContribution(response);
-                       headerRendering = false;
-               }
-       }
-
-       private void writeHeaderContribution(Response response)
-       {
-               if (encodingHeaderResponse.getContents().length() != 0)
-               {
-                       response.write("<header-contribution");
-
-                       if (encodingHeaderResponse.isContentsEncoded())
-                       {
-                               response.write(" encoding=\"");
-                               response.write(getEncodingName());
-                               response.write("\" ");
-                       }
-
-                       // we need to write response as CDATA and parse it on 
client,
-                       // because konqueror crashes when there is a <script> 
element
-                       response.write("><![CDATA[<head 
xmlns:wicket=\"http://wicket.apache.org\";>");
-                       response.write(encodingHeaderResponse.getContents());
-                       response.write("</head>]]>");
-                       response.write("</header-contribution>");
-               }
-       }
-
-       /**
-        * Checks if the target contains an ancestor for the given component
-        *
-        * @param component
-        * @return <code>true</code> if target contains an ancestor for the 
given component
-        */
-       private boolean containsAncestorFor(Component component)
-       {
-               Component cursor = component.getParent();
-               while (cursor != null)
-               {
-                       if (markupIdToComponent.containsValue(cursor))
-                       {
-                               return true;
-                       }
-                       cursor = cursor.getParent();
-               }
-               return false;
-       }
-
-       /**
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString()
        {
-               return "[AjaxRequestHandler@" + hashCode() + " 
markupIdToComponent [" + markupIdToComponent +
-                       "], prependJavaScript [" + prependJavaScripts + "], 
appendJavaScript [" +
-                       appendJavaScripts + "]";
-       }
-
-       /**
-        * Encodes a string so it is safe to use inside CDATA blocks
-        *
-        * @param str
-        * @return encoded string
-        */
-       private static String encode(CharSequence str)
-       {
-               if (str == null)
-               {
-                       return null;
-               }
-
-               return Strings.replaceAll(str, "]", "]^").toString();
-       }
-
-       /**
-        * @return name of encoding used to possibly encode the contents of the 
CDATA blocks
-        */
-       protected String getEncodingName()
-       {
-               return "wicket1";
-       }
-
-       /**
-        *
-        * @param str
-        * @return true if string needs to be encoded, false otherwise
-        */
-       private static boolean needsEncoding(CharSequence str)
-       {
-               /*
-                * TODO Post 1.2: Ajax: we can improve this by keeping a buffer 
of at least 3 characters and
-                * checking that buffer so that we can narrow down escaping 
occurring only for ']]>'
-                * sequence, or at least for ]] if ] is the last char in this 
buffer.
-                *
-                * but this improvement will only work if we write first and 
encode later instead of working
-                * on fragments sent to write
-                */
-               return Strings.indexOf(str, ']') >= 0;
-       }
-
-       /**
-        *
-        * @param response
-        * @param markupId
-        *            id of client-side dom element
-        * @param component
-        *            component to render
-        */
-       private void respondComponent(final Response response, final String 
markupId,
-               final Component component)
-       {
-               if (component.getRenderBodyOnly() == true)
-               {
-                       throw new IllegalStateException(
-                               "Ajax render cannot be called on component that 
has setRenderBodyOnly enabled. Component: " +
-                                       component.toString());
-               }
-
-               component.setOutputMarkupId(true);
-
-               // substitute our encoding response for the real one so we can 
capture
-               // component's markup in a manner safe for transport inside 
CDATA block
-               encodingBodyResponse.reset();
-               RequestCycle.get().setResponse(encodingBodyResponse);
-
-               // Initialize temporary variables
-               final 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: 
" + markupId +
-                               " not rendered because it was already removed 
from page");
-                       return;
-               }
-
-               page.startComponentRender(component);
-
-               try
-               {
-                       component.prepareForRender();
-
-                       // render any associated headers of the component
-                       respondHeaderContribution(response, component);
-               }
-               catch (RuntimeException e)
-               {
-                       try
-                       {
-                               component.afterRender();
-                       }
-                       catch (RuntimeException e2)
-                       {
-                               // ignore this one could be a result off.
-                       }
-                       // Restore original response
-                       RequestCycle.get().setResponse(response);
-                       encodingBodyResponse.reset();
-                       throw e;
-               }
-
-               try
-               {
-                       component.render();
-               }
-               catch (RuntimeException e)
-               {
-                       RequestCycle.get().setResponse(response);
-                       encodingBodyResponse.reset();
-                       throw e;
-               }
-
-               page.endComponentRender(component);
-
-               // Restore original response
-               RequestCycle.get().setResponse(response);
-
-               response.write("<component id=\"");
-               response.write(markupId);
-               response.write("\" ");
-               if (encodingBodyResponse.isContentsEncoded())
-               {
-                       response.write(" encoding=\"");
-                       response.write(getEncodingName());
-                       response.write("\" ");
-               }
-               response.write("><![CDATA[");
-               response.write(encodingBodyResponse.getContents());
-               response.write("]]></component>");
-
-               encodingBodyResponse.reset();
-       }
-
-       /**
-        * Header response for an ajax request.
-        *
-        * @author Matej Knopp
-        */
-       private class AjaxHeaderResponse extends HeaderResponse
-       {
-               @Override
-               public void render(HeaderItem item)
-               {
-                       if (item instanceof OnLoadHeaderItem)
-                       {
-                               if (!wasItemRendered(item))
-                               {
-                                       
appendJavaScripts.add(((OnLoadHeaderItem)item).getJavaScript());
-                                       markItemRendered(item);
-                               }
-                       }
-                       else if (item instanceof OnDomReadyHeaderItem)
-                       {
-                               if (!wasItemRendered(item))
-                               {
-                                       
domReadyJavaScripts.add(((OnDomReadyHeaderItem)item).getJavaScript());
-                                       markItemRendered(item);
-                               }
-                       }
-                       else if (headerRendering)
-                               super.render(item);
-                       else
-                               log.debug("Only methods that can be called on 
IHeaderResponse outside renderHead() are renderOnLoadJavaScript and 
renderOnDomReadyJavaScript");
-               }
-
-               /**
-                * Construct.
-                */
-               public AjaxHeaderResponse()
-               {
-               }
-
-               /**
-                *
-                * @see 
org.apache.wicket.markup.head.internal.HeaderResponse#getRealResponse()
-                */
-               @Override
-               protected Response getRealResponse()
-               {
-                       return RequestCycle.get().getResponse();
-               }
+               return "[AjaxRequestHandler@" + hashCode() + " responseObject 
[" + responseObject + "]";
        }
 
-       // whether a header contribution is being rendered
-       private boolean headerRendering = false;
-       private HtmlHeaderContainer header = null;
-
-       private IHeaderResponse headerResponse;
-
        @Override
        public IHeaderResponse getHeaderResponse()
        {
-               if (headerResponse == null)
-               {
-                       // we don't need to decorate the header response here 
because this is called from
-                       // within AjaxHtmlHeaderContainer, which decorates the 
response
-                       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, AjaxRequestHandler 
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 AjaxRequestHandler target;
+               return responseObject.getHeaderResponse();
        }
 
        /**
         *
-        * @param response
-        * @param component
-        */
-       private void respondHeaderContribution(final Response response, final 
Component component)
-       {
-               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
-               Response oldResponse = 
RequestCycle.get().setResponse(encodingHeaderResponse);
-
-               encodingHeaderResponse.reset();
-
-               // render the head of component and all it's children
-
-               component.renderHead(header);
-
-               if (component instanceof MarkupContainer)
-               {
-                       ((MarkupContainer)component).visitChildren(new 
IVisitor<Component, Void>()
-                       {
-                               @Override
-                               public void component(final Component 
component, final IVisit<Void> visit)
-                               {
-                                       if (component.isVisibleInHierarchy())
-                                       {
-                                               component.renderHead(header);
-                                       }
-                                       else
-                                       {
-                                               visit.dontGoDeeper();
-                                       }
-                               }
-                       });
-               }
-
-               // revert to old response
-               RequestCycle.get().setResponse(oldResponse);
-
-               writeHeaderContribution(response);
-
-               headerRendering = false;
-       }
-
-       private void respondInvocation(final Response response, final 
CharSequence js)
-       {
-               respondJavascriptInvocation("evaluate", response, js);
-       }
-
-       private void respondPriorityInvocation(final Response response, final 
CharSequence js)
-       {
-               respondJavascriptInvocation("priority-evaluate", response, js);
-       }
-
-
-       /**
-        * @param invocation
-        *            type of invocation tag, usually {@literal evaluate} or
-        *            {@literal priority-evaluate}
-        * @param response
-        * @param js
+        * @return
         */
-       private void respondJavascriptInvocation(final String invocation, final 
Response response,
-               final CharSequence js)
-       {
-               boolean encoded = false;
-               CharSequence javascript = js;
-
-               // encode the response if needed
-               if (needsEncoding(js))
-               {
-                       encoded = true;
-                       javascript = encode(js);
-               }
-
-               response.write("<");
-               response.write(invocation);
-               if (encoded)
-               {
-                       response.write(" encoding=\"");
-                       response.write(getEncodingName());
-                       response.write("\"");
-               }
-               response.write(">");
-
-               response.write("<![CDATA[");
-               response.write(javascript);
-               response.write("]]>");
-
-               response.write("</");
-               response.write(invocation);
-               response.write(">");
-
-               encodingBodyResponse.reset();
-       }
-
        @Override
        public String getLastFocusedElementId()
        {
-               WebRequest request = 
(WebRequest)RequestCycle.get().getRequest();
+               WebRequest request = (WebRequest) page.getRequest();
                String id = request.getHeader("Wicket-FocusedElementId");
                return Strings.isEmpty(id) ? null : id;
        }
@@ -1126,4 +469,23 @@ public class AjaxRequestHandler implements 
AjaxRequestTarget
        {
                return logData;
        }
+
+       private void assertNotFrozen(boolean frozen, Class<?> clazz)
+       {
+               if (frozen)
+               {
+                       throw new IllegalStateException(clazz.getSimpleName() + 
"s can no " +
+                                       " longer be added");
+               }
+       }
+
+       private void assertRespondersNotFrozen()
+       {
+               assertNotFrozen(respondersFrozen, 
AjaxRequestTarget.ITargetRespondListener.class);
+       }
+
+       private void assertListenersNotFrozen()
+       {
+               assertNotFrozen(listenersFrozen, 
AjaxRequestTarget.IListener.class);
+       }
 }

http://git-wip-us.apache.org/repos/asf/wicket/blob/fda7e7a0/wicket-core/src/main/java/org/apache/wicket/ajax/XmlAjaxResponse.java
----------------------------------------------------------------------
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/XmlAjaxResponse.java 
b/wicket-core/src/main/java/org/apache/wicket/ajax/XmlAjaxResponse.java
new file mode 100644
index 0000000..f41d0e9
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/ajax/XmlAjaxResponse.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.ajax;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.Page;
+import org.apache.wicket.request.Response;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.http.WebResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An AbstractAjaxResponse that serializes itself to XML.
+ * <p>
+ *     The elements of such response are:
+ *     <ul>
+ *         <li>priority-evaluate - an item of the prepend JavaScripts</li>
+ *         <li>component - the markup of the updated component</li>
+ *         <li>evaluate - an item of the onDomReady and append JavaScripts</li>
+ *         <li>header-contribution - all HeaderItems which have been 
contributed in components'
+ *         and their behaviors' #renderHead(Component, IHeaderResponse)</li>
+ *     </ul>
+ * </p>
+ */
+abstract class XmlAjaxResponse extends AbstractAjaxResponse
+{
+       private static final Logger LOG = 
LoggerFactory.getLogger(XmlAjaxResponse.class);
+
+       XmlAjaxResponse(final Page page)
+       {
+               super(page);
+       }
+
+       @Override
+       public void setContentType(WebResponse response, String encoding)
+       {
+               response.setContentType("text/xml; charset=" + encoding);
+       }
+
+       @Override
+       protected void writeHeader(Response response, String encoding)
+       {
+               response.write("<?xml version=\"1.0\" encoding=\"");
+               response.write(encoding);
+               response.write("\"?>");
+               response.write("<ajax-response>");
+       }
+
+       @Override
+       protected void writeComponent(Response response, String markupId, 
Component component, String encoding)
+       {
+               if (component.getRenderBodyOnly() == true)
+               {
+                       throw new IllegalStateException(
+                                       "Ajax render cannot be called on 
component that has setRenderBodyOnly enabled. Component: " +
+                                                       component.toString());
+               }
+
+               component.setOutputMarkupId(true);
+
+               // substitute our encoding response for the real one so we can 
capture
+               // component's markup in a manner safe for transport inside 
CDATA block
+               encodingBodyResponse.reset();
+               RequestCycle.get().setResponse(encodingBodyResponse);
+
+               // Initialize temporary variables
+               final 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: 
" + markupId +
+                                       " not rendered because it was already 
removed from page");
+                       return;
+               }
+
+               page.startComponentRender(component);
+
+               try
+               {
+                       component.prepareForRender();
+
+                       // render any associated headers of the component
+                       writeHeaderContribution(response, component);
+               }
+               catch (RuntimeException e)
+               {
+                       try
+                       {
+                               component.afterRender();
+                       }
+                       catch (RuntimeException e2)
+                       {
+                               // ignore this one could be a result off.
+                       }
+                       // Restore original response
+                       RequestCycle.get().setResponse(response);
+                       encodingBodyResponse.reset();
+                       throw e;
+               }
+
+               try
+               {
+                       component.render();
+               }
+               catch (RuntimeException e)
+               {
+                       RequestCycle.get().setResponse(response);
+                       encodingBodyResponse.reset();
+                       throw e;
+               }
+
+               page.endComponentRender(component);
+
+               // Restore original response
+               RequestCycle.get().setResponse(response);
+
+               response.write("<component id=\"");
+               response.write(markupId);
+               response.write("\" ");
+               if (encodingBodyResponse.isContentsEncoded())
+               {
+                       response.write(" encoding=\"");
+                       response.write(getEncodingName());
+                       response.write("\" ");
+               }
+               response.write("><![CDATA[");
+               response.write(encodingBodyResponse.getContents());
+               response.write("]]></component>");
+
+               encodingBodyResponse.reset();
+       }
+
+       @Override
+       protected void writeFooter(Response response, String encoding)
+       {
+               response.write("</ajax-response>");
+       }
+
+       @Override
+       protected void writeHeaderContribution(Response response)
+       {
+               if (encodingHeaderResponse.getContents().length() != 0)
+               {
+                       response.write("<header-contribution");
+
+                       if (encodingHeaderResponse.isContentsEncoded())
+                       {
+                               response.write(" encoding=\"");
+                               response.write(getEncodingName());
+                               response.write("\" ");
+                       }
+
+                       // we need to write response as CDATA and parse it on 
client,
+                       // because konqueror crashes when there is a <script> 
element
+                       response.write("><![CDATA[<head 
xmlns:wicket=\"http://wicket.apache.org\";>");
+                       response.write(encodingHeaderResponse.getContents());
+                       response.write("</head>]]>");
+                       response.write("</header-contribution>");
+               }
+       }
+
+       @Override
+       protected void writeNormalEvaluation(final Response response, final 
CharSequence js)
+       {
+               writeEvaluation("evaluate", response, js);
+       }
+
+       @Override
+       protected void writePriorityEvaluation(Response response, CharSequence 
js)
+       {
+               writeEvaluation("priority-evaluate", response, js);
+       }
+
+       /**
+       * @param invocation
+       *            type of invocation tag, usually {@literal evaluate} or
+       *            {@literal priority-evaluate}
+       * @param response
+       * @param js
+       */
+       private void writeEvaluation(final String invocation, final Response 
response, final CharSequence js)
+       {
+               boolean encoded = false;
+               CharSequence javascript = js;
+
+               // encode the response if needed
+               if (needsEncoding(js))
+               {
+                       encoded = true;
+                       javascript = encode(js);
+               }
+
+               response.write("<");
+               response.write(invocation);
+               if (encoded)
+               {
+                       response.write(" encoding=\"");
+                       response.write(getEncodingName());
+                       response.write("\"");
+               }
+               response.write(">");
+
+               response.write("<![CDATA[");
+               response.write(javascript);
+               response.write("]]>");
+
+               response.write("</");
+               response.write(invocation);
+               response.write(">");
+
+               encodingBodyResponse.reset();
+       }
+
+}

Reply via email to