001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.ajax; 018 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashSet; 022import java.util.LinkedHashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import org.apache.wicket.Application; 028import org.apache.wicket.Component; 029import org.apache.wicket.Page; 030import org.apache.wicket.core.request.handler.AbstractPartialPageRequestHandler; 031import org.apache.wicket.core.request.handler.PageProvider; 032import org.apache.wicket.core.request.handler.RenderPageRequestHandler; 033import org.apache.wicket.core.request.handler.logger.PageLogData; 034import org.apache.wicket.event.Broadcast; 035import org.apache.wicket.page.PartialPageUpdate; 036import org.apache.wicket.page.XmlPartialPageUpdate; 037import org.apache.wicket.request.IRequestCycle; 038import org.apache.wicket.request.IRequestHandler; 039import org.apache.wicket.request.Response; 040import org.apache.wicket.request.cycle.RequestCycle; 041import org.apache.wicket.request.http.WebRequest; 042import org.apache.wicket.request.http.WebResponse; 043import org.apache.wicket.response.StringResponse; 044import org.apache.wicket.response.filter.IResponseFilter; 045import org.apache.wicket.util.encoding.UrlDecoder; 046import org.apache.wicket.util.lang.Args; 047import org.apache.wicket.util.lang.Classes; 048import org.apache.wicket.util.string.AppendingStringBuffer; 049import org.apache.wicket.util.string.Strings; 050 051/** 052 * A request target that produces ajax response envelopes used on the client side to update 053 * component markup as well as evaluate arbitrary javascript. 054 * <p> 055 * A component whose markup needs to be updated should be added to this target via 056 * AjaxRequestTarget#add(Component) method. Its body will be rendered and added to the envelope when 057 * the target is processed, and refreshed on the client side when the ajax response is received. 058 * <p> 059 * It is important that the component whose markup needs to be updated contains an id attribute in 060 * the generated markup that is equal to the value retrieved from Component#getMarkupId(). This can 061 * be accomplished by either setting the id attribute in the html template, or using an attribute 062 * modifier that will add the attribute with value Component#getMarkupId() to the tag ( such as 063 * MarkupIdSetter ) 064 * <p> 065 * Any javascript that needs to be evaluated on the client side can be added using 066 * AjaxRequestTarget#append/prependJavaScript(String). For example, this feature can be useful when 067 * it is desirable to link component update with some javascript effects. 068 * <p> 069 * The target provides a listener interface {@link AjaxRequestTarget.IListener} that can be used to 070 * add code that responds to various target events by adding listeners via 071 * {@link #addListener(AjaxRequestTarget.IListener)} 072 * 073 * @since 1.2 074 * 075 * @author Igor Vaynberg (ivaynberg) 076 * @author Eelco Hillenius 077 */ 078public class AjaxRequestHandler extends AbstractPartialPageRequestHandler implements AjaxRequestTarget 079{ 080 /** 081 * Collector of page updates. 082 */ 083 private final PartialPageUpdate update; 084 085 /** a set of listeners */ 086 private Set<AjaxRequestTarget.IListener> listeners = null; 087 088 /** */ 089 private final Set<ITargetRespondListener> respondListeners = new HashSet<>(); 090 091 /** see https://issues.apache.org/jira/browse/WICKET-3564 */ 092 protected transient boolean respondersFrozen; 093 protected transient boolean listenersFrozen; 094 095 private PageLogData logData; 096 097 /** 098 * Constructor 099 * 100 * @param page 101 * the currently active page 102 */ 103 public AjaxRequestHandler(final Page page) 104 { 105 super(page); 106 107 update = new XmlPartialPageUpdate(page) 108 { 109 /** 110 * Freezes the {@link AjaxRequestHandler#listeners} before firing the event and 111 * un-freezes them afterwards to allow components to add more 112 * {@link AjaxRequestTarget.IListener}s for the second event. 113 */ 114 @Override 115 protected void onBeforeRespond(final Response response) 116 { 117 listenersFrozen = true; 118 119 if (listeners != null) 120 { 121 for (AjaxRequestTarget.IListener listener : listeners) 122 { 123 listener.onBeforeRespond(markupIdToComponent, AjaxRequestHandler.this); 124 } 125 } 126 127 listenersFrozen = false; 128 } 129 130 /** 131 * Freezes the {@link AjaxRequestHandler#listeners}, and does not un-freeze them as the 132 * events will have been fired by now. 133 * 134 * @param response 135 * the response to write to 136 */ 137 @Override 138 protected void onAfterRespond(final Response response) 139 { 140 listenersFrozen = true; 141 142 // invoke onAfterRespond event on listeners 143 if (listeners != null) 144 { 145 final Map<String, Component> components = Collections 146 .unmodifiableMap(markupIdToComponent); 147 148 for (AjaxRequestTarget.IListener listener : listeners) 149 { 150 listener.onAfterRespond(components, AjaxRequestHandler.this); 151 } 152 } 153 } 154 }; 155 } 156 157 @Override 158 public void addListener(AjaxRequestTarget.IListener listener) throws IllegalStateException 159 { 160 Args.notNull(listener, "listener"); 161 assertListenersNotFrozen(); 162 163 if (listeners == null) 164 { 165 listeners = new LinkedHashSet<>(); 166 } 167 168 if (!listeners.contains(listener)) 169 { 170 listeners.add(listener); 171 } 172 } 173 174 @Override 175 public PartialPageUpdate getUpdate() 176 { 177 return update; 178 } 179 180 @Override 181 public final Collection<? extends Component> getComponents() 182 { 183 return update.getComponents(); 184 } 185 186 /** 187 * @see org.apache.wicket.core.request.handler.IPageRequestHandler#detach(org.apache.wicket.request.IRequestCycle) 188 */ 189 @Override 190 public void detach(final IRequestCycle requestCycle) 191 { 192 if (logData == null) 193 { 194 logData = new PageLogData(getPage()); 195 } 196 197 update.detach(requestCycle); 198 } 199 200 /** 201 * @see java.lang.Object#equals(java.lang.Object) 202 */ 203 @Override 204 public boolean equals(final Object obj) 205 { 206 if (obj instanceof AjaxRequestHandler) 207 { 208 AjaxRequestHandler that = (AjaxRequestHandler)obj; 209 return update.equals(that.update); 210 } 211 return false; 212 } 213 214 /** 215 * @see java.lang.Object#hashCode() 216 */ 217 @Override 218 public int hashCode() 219 { 220 int result = "AjaxRequestHandler".hashCode(); 221 result += update.hashCode() * 17; 222 return result; 223 } 224 225 @Override 226 public void registerRespondListener(ITargetRespondListener listener) 227 { 228 assertRespondersNotFrozen(); 229 respondListeners.add(listener); 230 } 231 232 /** 233 * @see org.apache.wicket.core.request.handler.IPageRequestHandler#respond(org.apache.wicket.request.IRequestCycle) 234 */ 235 @Override 236 public final void respond(final IRequestCycle requestCycle) 237 { 238 final RequestCycle rc = (RequestCycle)requestCycle; 239 final WebResponse response = (WebResponse)requestCycle.getResponse(); 240 241 Page page = getPage(); 242 243 if (shouldRedirectToPage(requestCycle)) 244 { 245 // the page itself has been added to the request target, we simply issue a redirect 246 // back to the page 247 IRequestHandler handler = new RenderPageRequestHandler(new PageProvider(page)); 248 final String url = rc.urlFor(handler).toString(); 249 response.sendRedirect(url); 250 return; 251 } 252 253 respondersFrozen = true; 254 255 for (ITargetRespondListener listener : respondListeners) 256 { 257 listener.onTargetRespond(this); 258 } 259 260 final Application app = page.getApplication(); 261 262 page.send(app, Broadcast.BREADTH, this); 263 264 // Determine encoding 265 final String encoding = app.getRequestCycleSettings().getResponseRequestEncoding(); 266 267 // Set content type based on markup type for page 268 update.setContentType(response, encoding); 269 270 // Make sure it is not cached by a client 271 response.disableCaching(); 272 273 final List<IResponseFilter> filters = Application.get() 274 .getRequestCycleSettings() 275 .getResponseFilters(); 276 // WICKET-7074 we need to write to a temporary buffer, otherwise, if an exception is produced, 277 // and a redirect is done we will end up with a malformed XML 278 final StringResponse bodyResponse = new StringResponse(); 279 update.writeTo(bodyResponse, encoding); 280 if (filters == null || filters.isEmpty()) 281 { 282 response.write(bodyResponse.getBuffer()); 283 } 284 else 285 { 286 CharSequence filteredResponse = invokeResponseFilters(bodyResponse, filters); 287 response.write(filteredResponse); 288 } 289 } 290 291 private boolean shouldRedirectToPage(IRequestCycle requestCycle) 292 { 293 if (update.containsPage()) 294 { 295 return true; 296 } 297 298 if (((WebRequest)requestCycle.getRequest()).isAjax() == false) 299 { 300 // the request was not sent by wicket-ajax.js - this can happen when an Ajax request was 301 // intercepted with #redirectToInterceptPage() and then the original request is re-sent 302 // by the browser on a subsequent #continueToOriginalDestination() 303 return true; 304 } 305 306 return false; 307 } 308 309 /** 310 * Runs the configured {@link IResponseFilter}s over the constructed Ajax response 311 * 312 * @param contentResponse 313 * the Ajax {@link Response} body 314 * @param responseFilters 315 * the response filters 316 * @return filtered response 317 */ 318 private CharSequence invokeResponseFilters(final StringResponse contentResponse, 319 final List<IResponseFilter> responseFilters) 320 { 321 AppendingStringBuffer responseBuffer = new AppendingStringBuffer( 322 contentResponse.getBuffer()); 323 for (IResponseFilter filter : responseFilters) 324 { 325 responseBuffer = filter.filter(responseBuffer); 326 } 327 return responseBuffer; 328 } 329 330 /** 331 * @see java.lang.Object#toString() 332 */ 333 @Override 334 public String toString() 335 { 336 return "[AjaxRequestHandler@" + hashCode() + " responseObject [" + update + "]"; 337 } 338 339 /** 340 * @return the markup id of the focused element in the browser 341 */ 342 @Override 343 public String getLastFocusedElementId() 344 { 345 WebRequest request = (WebRequest)getPage().getRequest(); 346 347 String id = request.getHeader("Wicket-FocusedElementId"); 348 349 // WICKET-6568 might contain non-ASCII 350 return Strings.isEmpty(id) ? null : UrlDecoder.QUERY_INSTANCE.decode(id, request.getCharset()); 351 } 352 353 @Override 354 public PageLogData getLogData() 355 { 356 return logData; 357 } 358 359 private void assertNotFrozen(boolean frozen, Class<?> clazz) 360 { 361 if (frozen) 362 { 363 throw new IllegalStateException(Classes.simpleName(clazz) + "s can no longer be added"); 364 } 365 } 366 367 private void assertRespondersNotFrozen() 368 { 369 assertNotFrozen(respondersFrozen, AjaxRequestTarget.ITargetRespondListener.class); 370 } 371 372 private void assertListenersNotFrozen() 373 { 374 assertNotFrozen(listenersFrozen, AjaxRequestTarget.IListener.class); 375 } 376}