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}