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(); + } + +}
