001: /*
002: * $Id: AjaxRequestTarget.java 4837 2006-03-08 14:46:58 -0800 (Wed, 08 Mar 2006)
003: * ivaynberg $ $Revision: 508111 $ $Date: 2006-03-08 14:46:58 -0800 (Wed, 08 Mar
004: * 2006) $
005: *
006: * ==============================================================================
007: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
008: * use this file except in compliance with the License. You may obtain a copy of
009: * the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software
014: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
015: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
016: * License for the specific language governing permissions and limitations under
017: * the License.
018: */
019: package wicket.ajax;
020:
021: import java.io.OutputStream;
022: import java.util.ArrayList;
023: import java.util.HashMap;
024: import java.util.Iterator;
025: import java.util.List;
026: import java.util.Map;
027: import java.util.Map.Entry;
028:
029: import org.apache.commons.logging.Log;
030: import org.apache.commons.logging.LogFactory;
031:
032: import wicket.Application;
033: import wicket.Component;
034: import wicket.IRequestTarget;
035: import wicket.MarkupContainer;
036: import wicket.Page;
037: import wicket.RequestCycle;
038: import wicket.Response;
039: import wicket.markup.html.internal.HtmlHeaderContainer;
040: import wicket.markup.parser.filter.HtmlHeaderSectionHandler;
041: import wicket.protocol.http.WebResponse;
042: import wicket.util.string.AppendingStringBuffer;
043: import wicket.util.string.Strings;
044:
045: /**
046: * A request target that produces ajax response envelopes used on the client
047: * side to update component markup as well as evaluate arbitrary javascript.
048: * <p>
049: * A component whose markup needs to be updated should be added to this target
050: * via AjaxRequestTarget#addComponent(Component) method. Its body will be
051: * rendered and added to the envelope when the target is processed, and
052: * refreshed on the client side when the ajax response is received.
053: * <p>
054: * It is important that the component whose markup needs to be updated contains
055: * an id attribute in the generated markup that is equal to the value retrieved
056: * from Component#getMarkupId(). This can be accomplished by either setting the
057: * id attribute in the html template, or using an attribute modifier that will
058: * add the attribute with value Component#getMarkupId() to the tag ( such as
059: * MarkupIdSetter )
060: * <p>
061: * Any javascript that needs to be evaluater on the client side can be added
062: * using AjaxRequestTarget#addJavascript(String). For example, this feature can
063: * be useful when it is desirable to link component update with some javascript
064: * effects.
065: *
066: * @since 1.2
067: *
068: * @author Igor Vaynberg (ivaynberg)
069: * @author Eelco Hillenius
070: */
071: public class AjaxRequestTarget implements IRequestTarget {
072: /**
073: * Response that uses an encoder to encode its contents
074: *
075: * @author Igor Vaynberg (ivaynberg)
076: */
077: private final class EncodingResponse extends Response {
078: private final AppendingStringBuffer buffer = new AppendingStringBuffer(
079: 256);
080:
081: private boolean escaped = false;
082:
083: private final Response originalResponse;
084:
085: /**
086: * Construct.
087: *
088: * @param originalResponse
089: */
090: public EncodingResponse(Response originalResponse) {
091: this .originalResponse = originalResponse;
092: }
093:
094: /**
095: * @see wicket.Response#encodeURL(CharSequence)
096: */
097: public CharSequence encodeURL(CharSequence url) {
098: return originalResponse.encodeURL(url);
099: }
100:
101: /**
102: * @return contents of the response
103: */
104: public CharSequence getContents() {
105: return buffer;
106: }
107:
108: /**
109: * NOTE: this method is not supported
110: *
111: * @see wicket.Response#getOutputStream()
112: */
113: public OutputStream getOutputStream() {
114: throw new UnsupportedOperationException(
115: "Cannot get output stream on StringResponse");
116: }
117:
118: /**
119: * @return true if any escaping has been performed, false otherwise
120: */
121: public boolean isContentsEncoded() {
122: return escaped;
123: }
124:
125: /**
126: * Resets the response to a clean state so it can be reused to save on
127: * garbage.
128: */
129: public void reset() {
130: buffer.clear();
131: escaped = false;
132:
133: }
134:
135: /**
136: * @see wicket.Response#write(CharSequence)
137: */
138: public void write(CharSequence cs) {
139: String string = cs.toString();
140: if (needsEncoding(string)) {
141: string = encode(string);
142: escaped = true;
143: buffer.append(string);
144: } else {
145: buffer.append(cs);
146: }
147: }
148:
149: }
150:
151: private static final Log LOG = LogFactory
152: .getLog(AjaxRequestTarget.class);
153:
154: private final List/* <String> */appendJavascripts = new ArrayList();
155:
156: /**
157: * Create a response for component body and javascript that will escape
158: * output to make it safe to use inside a CDATA block
159: */
160: private final EncodingResponse encodingBodyResponse;
161:
162: /**
163: * Response for header contributon that will escape output to make it safe
164: * to use inside a CDATA block
165: */
166: private final EncodingResponse encodingHeaderResponse;
167:
168: /** the component instances that will be rendered */
169: private final Map/* <String,Component> */markupIdToComponent = new HashMap();
170:
171: private final List/* <String> */prependJavascripts = new ArrayList();
172:
173: /**
174: * Constructor
175: */
176: public AjaxRequestTarget() {
177: Response response = RequestCycle.get().getResponse();
178: encodingBodyResponse = new EncodingResponse(response);
179: encodingHeaderResponse = new EncodingResponse(response);
180: }
181:
182: /**
183: * Adds a component to the list of components to be rendered
184: *
185: * @param component
186: * component to be rendered
187: */
188: public final void addComponent(Component component) {
189: addComponent(component, component.getMarkupId());
190: }
191:
192: /**
193: * Adds a component to the list of components to be rendered
194: *
195: * @param markupId
196: * id of client-side dom element that will be updated
197: *
198: * @param component
199: * component to be rendered
200: */
201: public final void addComponent(Component component, String markupId) {
202: if (Strings.isEmpty(markupId)) {
203: throw new IllegalArgumentException(
204: "markupId cannot be empty");
205: }
206: if (component == null) {
207: throw new IllegalArgumentException(
208: "component cannot be null");
209: } else if (component instanceof Page) {
210: throw new IllegalArgumentException(
211: "component cannot be a page");
212: }
213:
214: markupIdToComponent.put(markupId, component);
215: }
216:
217: /**
218: * Adds javascript that will be evaluated on the client side after
219: * components are replaced
220: *
221: * @deprecated use appendJavascript(String javascript) instead
222: * @param javascript
223: */
224: public final void addJavascript(String javascript) {
225: appendJavascript(javascript);
226: }
227:
228: /**
229: * Adds javascript that will be evaluated on the client side after
230: * components are replaced
231: *
232: * @param javascript
233: */
234: public final void appendJavascript(String javascript) {
235: if (javascript == null) {
236: throw new IllegalArgumentException(
237: "javascript cannot be null");
238: }
239:
240: appendJavascripts.add(javascript);
241: }
242:
243: /**
244: * @see wicket.IRequestTarget#detach(wicket.RequestCycle)
245: */
246: public void detach(final RequestCycle requestCycle) {
247: }
248:
249: /**
250: * @see java.lang.Object#equals(java.lang.Object)
251: */
252: public boolean equals(final Object obj) {
253: if (obj instanceof AjaxRequestTarget) {
254: AjaxRequestTarget that = (AjaxRequestTarget) obj;
255: return markupIdToComponent.equals(that.markupIdToComponent)
256: && prependJavascripts
257: .equals(that.prependJavascripts)
258: && appendJavascripts.equals(that.appendJavascripts);
259: }
260: return false;
261: }
262:
263: /**
264: * @see wicket.IRequestTarget#getLock(RequestCycle)
265: */
266: public Object getLock(final RequestCycle requestCycle) {
267: return requestCycle.getSession();
268: }
269:
270: /**
271: * @see java.lang.Object#hashCode()
272: */
273: public int hashCode() {
274: int result = "AjaxRequestTarget".hashCode();
275: result += markupIdToComponent.hashCode() * 17;
276: result += prependJavascripts.hashCode() * 17;
277: result += appendJavascripts.hashCode() * 17;
278: return result;
279: }
280:
281: /**
282: * Adds javascript that will be evaluated on the client side before
283: * components are replaced
284: *
285: * @param javascript
286: */
287: public final void prependJavascript(String javascript) {
288: if (javascript == null) {
289: throw new IllegalArgumentException(
290: "javascript cannot be null");
291: }
292:
293: prependJavascripts.add(javascript);
294: }
295:
296: /**
297: * @see wicket.IRequestTarget#respond(wicket.RequestCycle)
298: */
299: public final void respond(final RequestCycle requestCycle) {
300: try {
301: final Application app = Application.get();
302:
303: // Determine encoding
304: final String encoding = app.getRequestCycleSettings()
305: .getResponseRequestEncoding();
306:
307: // Set content type based on markup type for page
308: WebResponse response = (WebResponse) requestCycle
309: .getResponse();
310: response.setCharacterEncoding(encoding);
311: response.setContentType("text/xml; charset=" + encoding);
312:
313: // Make sure it is not cached by a client
314: response.setHeader("Expires",
315: "Mon, 26 Jul 1997 05:00:00 GMT");
316: response.setHeader("Cache-Control",
317: "no-cache, must-revalidate");
318: response.setHeader("Pragma", "no-cache");
319:
320: response.write("<?xml version=\"1.0\" encoding=\"");
321: response.write(encoding);
322: response.write("\"?>");
323: response.write("<ajax-response>");
324:
325: // normal behavior
326: Iterator it = prependJavascripts.iterator();
327: while (it.hasNext()) {
328: String js = (String) it.next();
329: respondInvocation(response, js);
330: }
331:
332: it = markupIdToComponent.entrySet().iterator();
333: while (it.hasNext()) {
334: final Map.Entry entry = (Entry) it.next();
335: final Component component = (Component) entry
336: .getValue();
337: final String markupId = (String) entry.getKey();
338:
339: respondComponent(response, markupId, component);
340: }
341:
342: it = appendJavascripts.iterator();
343: while (it.hasNext()) {
344: String js = (String) it.next();
345: respondInvocation(response, js);
346: }
347:
348: response.write("</ajax-response>");
349: } catch (RuntimeException ex) {
350: // log the error but output nothing in the response, parse
351: // failure
352: // of response will cause any javascript failureHandler to be
353: // invoked
354: LOG.error("Error while responding to an AJAX request: "
355: + toString(), ex);
356: } finally {
357: // clean up the page
358: if (markupIdToComponent.size() > 0) {
359: final Component c = (Component) markupIdToComponent
360: .values().iterator().next();
361: c.getPage().internalDetach();
362: }
363: }
364: }
365:
366: /**
367: * @see java.lang.Object#toString()
368: */
369: public String toString() {
370: return "[AjaxRequestTarget@" + hashCode()
371: + " markupIdToComponent [" + markupIdToComponent
372: + "], prependJavascript [" + prependJavascripts
373: + "], appendJavascript [" + appendJavascripts + "]";
374: }
375:
376: /**
377: * Encodes a string so it is safe to use inside CDATA blocks
378: *
379: * @param str
380: * @return encoded string
381: */
382: protected String encode(String str) {
383: // TODO Post 1.2: Java5: we can use str.replace(charseq, charseq) for
384: // more efficient replacement
385: return str.replaceAll("]", "]^");
386: }
387:
388: /**
389: * @return name of encoding used to possibly encode the contents of the
390: * CDATA blocks
391: */
392: protected String getEncodingName() {
393: return "wicket1";
394: }
395:
396: /**
397: *
398: * @param str
399: * @return true if string needs to be encoded, false otherwise
400: */
401: protected boolean needsEncoding(String str) {
402: /*
403: * TODO Post 1.2: Ajax: we can improve this by keeping a buffer of at
404: * least 3 characters and checking that buffer so that we can narrow
405: * down escaping occuring only for ']]>' sequence, or at least for ]] if ]
406: * is the last char in this buffer.
407: *
408: * but this improvement will only work if we write first and encode
409: * later instead of working on fragments sent to write
410: */
411:
412: return str.indexOf(']') >= 0;
413: }
414:
415: /**
416: *
417: * @param response
418: * @param markupId
419: * id of client-side dom element
420: * @param component
421: * component to render
422: */
423: private void respondComponent(final Response response,
424: final String markupId, final Component component) {
425: if (component.getRenderBodyOnly() == true) {
426: throw new IllegalStateException(
427: "Ajax render cannot be called on component that has setRenderBodyOnly enabled. Component: "
428: + component.toString());
429: }
430:
431: component.setOutputMarkupId(true);
432:
433: // substitute our encoding response for the real one so we can capture
434: // component's markup in a manner safe for transport inside CDATA block
435: final Response originalResponse = response;
436: encodingBodyResponse.reset();
437: RequestCycle.get().setResponse(encodingBodyResponse);
438:
439: // Initialize temporary variables
440: final Page page = component.getPage();
441: if (page == null) {
442: throw new IllegalStateException(
443: "Ajax request attempted on a component that is not associated with a Page");
444: }
445:
446: final boolean versioned = page.isVersioned();
447: page.setVersioned(false);
448:
449: page.startComponentRender(component);
450: component.renderComponent();
451:
452: respondHeaderContribution(response, component);
453:
454: page.endComponentRender(component);
455:
456: page.setVersioned(versioned);
457:
458: // Restore original response
459: RequestCycle.get().setResponse(originalResponse);
460:
461: response.write("<component id=\"");
462: response.write(markupId);
463: response.write("\" ");
464: if (encodingBodyResponse.isContentsEncoded()) {
465: response.write(" encoding=\"");
466: response.write(getEncodingName());
467: response.write("\" ");
468: }
469: response.write("><![CDATA[");
470: response.write(encodingBodyResponse.getContents());
471: response.write("]]></component>");
472:
473: encodingBodyResponse.reset();
474: }
475:
476: /**
477: *
478: * @param response
479: * @param component
480: */
481: private void respondHeaderContribution(final Response response,
482: final Component component) {
483: final HtmlHeaderContainer header = new HtmlHeaderContainer(
484: HtmlHeaderSectionHandler.HEADER_ID);
485: if (component.getPage().get(HtmlHeaderSectionHandler.HEADER_ID) != null) {
486: component.getPage().replace(header);
487: } else {
488: component.getPage().add(header);
489: }
490:
491: Response oldResponse = RequestCycle.get().setResponse(
492: encodingHeaderResponse);
493:
494: encodingHeaderResponse.reset();
495:
496: component.renderHead(header);
497: component.detachBehaviors();
498: if (component instanceof MarkupContainer) {
499: ((MarkupContainer) component)
500: .visitChildren(new Component.IVisitor() {
501: public Object component(Component component) {
502: if (component.isVisible()) {
503: component.renderHead(header);
504: component.detachBehaviors();
505: return CONTINUE_TRAVERSAL;
506: } else {
507: return CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
508: }
509: }
510: });
511: }
512:
513: RequestCycle.get().setResponse(oldResponse);
514:
515: if (encodingHeaderResponse.getContents().length() != 0) {
516: response.write("<header-contribution");
517:
518: if (encodingHeaderResponse.isContentsEncoded()) {
519: response.write(" encoding=\"");
520: response.write(getEncodingName());
521: response.write("\" ");
522: }
523:
524: // we need to write response as CDATA and parse it on client,
525: // because
526: // konqueror crashes when there is a <script> element
527: response
528: .write("><![CDATA[<head xmlns:wicket=\"http://wicket.sourceforge.net\">");
529:
530: response.write(encodingHeaderResponse.getContents());
531:
532: response.write("</head>]]>");
533:
534: response.write("</header-contribution>");
535: }
536: }
537:
538: /**
539: *
540: * @param response
541: * @param js
542: */
543: private void respondInvocation(final Response response,
544: final String js) {
545: boolean encoded = false;
546: String javascript = js;
547:
548: // encode the response if needed
549: if (needsEncoding(js)) {
550: encoded = true;
551: javascript = encode(js);
552: }
553:
554: response.write("<evaluate");
555: if (encoded) {
556: response.write(" encoding=\"");
557: response.write(getEncodingName());
558: response.write("\"");
559: }
560: response.write(">");
561: response.write("<![CDATA[");
562: response.write(javascript);
563: response.write("]]>");
564: response.write("</evaluate>");
565:
566: encodingBodyResponse.reset();
567: }
568: }
|