You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by mg...@apache.org on 2012/01/20 10:07:42 UTC

[2/2] git commit: WICKET-4349 Extract the code that creates the XML for Ajax responses out of AjaxRequestHandler

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 <mg...@apache.org>
Authored: Fri Jan 20 10:05:48 2012 +0100
Committer: Martin Tzvetanov Grigorov <mg...@apache.org>
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();
+	}
+
+}